swiftnstextviewnstask

Real time NSTask output to NSTextView with Swift


I'm using an NSTask to run rsync, and I'd like the status to show up in the text view of a scroll view inside a window. Right now I have this:

let pipe = NSPipe()
task2.standardOutput = pipe
task2.launch()

let data = pipe.fileHandleForReading.readDataToEndOfFile()
let output: String = NSString(data: data, encoding: NSASCIIStringEncoding)! as String

textView.string = output

And that get's me the some of the statistics about the transfer, but I'd like to get the output in real time, like what get's printed out when I run the app in Xcode, and put it into the text view. Is there a way to do this?


Solution

  • (See Patrick F.'s answer for an update to Swift 3/4.)

    You can read asynchronously from a pipe, using notifications. Here is a simple example demonstrating how it works, hopefully that helps you to get started:

    let task = NSTask()
    task.launchPath = "/bin/sh"
    task.arguments = ["-c", "echo 1 ; sleep 1 ; echo 2 ; sleep 1 ; echo 3 ; sleep 1 ; echo 4"]
    
    let pipe = NSPipe()
    task.standardOutput = pipe
    let outHandle = pipe.fileHandleForReading
    outHandle.waitForDataInBackgroundAndNotify()
    
    var obs1 : NSObjectProtocol!
    obs1 = NSNotificationCenter.defaultCenter().addObserverForName(NSFileHandleDataAvailableNotification,
        object: outHandle, queue: nil) {  notification -> Void in
            let data = outHandle.availableData
            if data.length > 0 {
                if let str = NSString(data: data, encoding: NSUTF8StringEncoding) {
                    print("got output: \(str)")
                }
                outHandle.waitForDataInBackgroundAndNotify()
            } else {
                print("EOF on stdout from process")
                NSNotificationCenter.defaultCenter().removeObserver(obs1)
            }
    }
    
    var obs2 : NSObjectProtocol!
    obs2 = NSNotificationCenter.defaultCenter().addObserverForName(NSTaskDidTerminateNotification,
        object: task, queue: nil) { notification -> Void in
            print("terminated")
            NSNotificationCenter.defaultCenter().removeObserver(obs2)
    }
    
    task.launch()
    

    Instead of print("got output: \(str)") you can append the received string to your text view.

    The above code assumes that a runloop is active (which is the case in a default Cocoa application).