NSTask Tutorial for Mac OS X

Andy Pereira Andy Pereira
See a practical example of using NSTask!

See a practical example of using NSTask!

Cocoa and Cocoa Touch have many similarities, which makes it easy to switch gears when you develop in both frameworks. Usually you only need to keep track of things like, “Is it text or stringValue?” Or, “Should it be UI or NS?”

However, every so often you run into some real gems while developing for OS X. NSTask is one of those rare bits that make writing apps for OS X really interesting.

NSTask allows you to execute another program on your machine as a subprocess and monitor its execution state while your main program continues to run. For example, you could run the ls command to display a directory listing – right from inside your app!

A good analogy for NSTask is a parent-child relationship. A parent can create a child, tell it to do certain things, and the child must obey. NSTask behaves in a similar fashion – you start a “child” program, give it instructions, and tell it where to report any output or errors.

A great use for NSTask is to provide a front-end GUI to command line programs. Command line programs are pretty powerful – but they demand a great memory to remember exactly where they live on your machine, how to call them, and what options or arguments you can provide to the program. Adding a GUI on the front end can provide the user with a great deal of control – without having to be a command line guru!

This tutorial includes two NSTask examples – one that shows you how to execute a simple command program with arguments, and one that shows you how to also display its standard output as it runs in a text field. By the end, you’ll know all about NSTasks and will be ready to use them in your own apps!

Note: This tutorial assumes you have some basic familiarity with Mac OSX development. If you are completely new to programming for the Mac, check out our beginning Mac OSX development tutorial series.

Introduction to the Command Line Interface

If you’ve never worked with a command line interface before, you’re missing out on some of the most interesting times you could be having on your computer! The Terminal app on your Mac gives you access to all kinds of command line goodness. If you don’t already know how to use it, give it a try now with the mini-tutorial below.

Note: Feel free to skip straight to the “Getting Started” section if you’re already familiar with Terminal.

Sit down at your Mac, open Finder, and browse into the Applications\Utilities folder where you’ll find the Terminal app. However, since you’re about to be a command line master, why not get used to typing? Use Spotlight — that little magnifying glass in the upper right corner of your screen — to search for “terminal”. As the following screenshot shows, you’ll probably find it before you’ve typed the whole word:

nstask_spotlight

Click the Terminal application in the results list, and you’ll be presented with a screen that looks like this:

ns_task_Terminal-Beginning

Make sure your computer’s volume is turned up, type the following command into the Terminal window, and hit Enter:

say Hello World

You should now hear your computer speaking to you! You’ve instructed Terminal to run the program named say, and provided it with some data: “Hello World.”

In the above command, the data “Hello World” is known as an argument. When the program is launched, it will use the various arguments passed to it when it is launched. It’s incredibly similar to passing in parameters when you call a method.

Arguments can be either required or optional, depending on the command. Failing to provide a required argument will usually result in an error message of some type.

Go back to Terminal, and type:

man say

You’ll be presented with the man page — short for “manual” page — for the say command as shown in the screenshot below:
ns_task_sayMan

man pages provide documentation for command line-based programs. Under the SYNOPSIS entry in the man page, you’ll see a list of the arguments that say accepts, as well as flags that can be provided.

What are “flags”, you ask? Flags are identifiers used to indicate the non-positional arguments provided to the program. For example, providing say with the -v flag followed by an argument will change the voice; the -r flag with a numeric argument will change the rate of the voice, and so on.

Pressing the space bar in Terminal will page through the various sections of the man document, where you can read a detailed description of each argument and how it is used. Type the letter q to exit the man page.

To see how flags and arguments work, try entering the following command into Terminal:

say -v vicki Hello World. I am Vicki.

In the command above, you passed in the value Vicki for the voice parameter delineated by the -v flag,followed by the string “Hello World. I am Vicki.”. And lo and behold, the computer speaks – only this time in Vicki’s voice. Well, the Mac OSX version of Vicki that is – not Ray’s wife!

Just for fun, type in the following command to see one of the secrets hidden on your Mac:

cat /usr/share/calendar/calendar.lotr

If you don’t know what cat means, try typing man cat to find out!

You’re probably starting to see the value in working with Terminal. It can do nearly anything you can think of, with the exception of that “fancy” GUI stuff.

Getting Started

At this point you should have sufficient background on command line apps, so let’s get to the meat of the tutorial – NSTask!

To keep the focus squarely on NSTask, I’ve created a starter project for you that contains a basic user interface. Download the project, open it in Xcode, and build and run.

You’ll see the starter app has two windows as shown in the screenshot below:

nstask_starter_app

The first window has the title “Talking” and the other has the title “TasksProject”. These windows aren’t related to each other; you’ll be using each one for a different part of this tutorial. If you don’t see both windows at first, try hunting around your desktop — sometimes the windows open behind other windows.

Creating Your First NSTask

Your first NSTask example will be implementing the window titled “Talking”. When the user clicks the “Speak” button you will run the “say” command line app you saw earlier to make the computer speak whatever the user put in the text field.

To do this, open AppDelegate.m and replace the empty method speak: with the following code:

- (IBAction)speak:(id)sender {
    //1
    NSTask *task = [[NSTask alloc] init];
 
    //2 
    task.launchPath = @"/usr/bin/say";
 
    //3 
    NSString* speakingPhrase = self.phraseField.stringValue;
    task.arguments  = @[speakingPhrase];
 
    //4
    [task launch];
 
    //5 
    [task waitUntilExit];
}

The above code does the following:

  1. Creates a new NSTask object. This object lets you instantiate and control external applications.
  2. Tells the NSTask object which program to run. You need to provide the complete path to the file so the NSTask can find it. In this case, the say program lives in your Mac’s /usr/bin/ directory.
  3. Sets the command line arguments to pass to the program. The say command doesn’t need a flag if you’re only passing in the words to be spoken, so in this case you simply create an array with a single element containing the text taken from the text field.
  4. Calls launch on the NSTask object to execute the program it was instructed to run.
  5. Calls waitUntilExit. This method begins monitoring the NSTask and will not return control to your program until the launched task is complete.

Build and run your app, and make sure your computer’s volume is turned up! Find the Talking window, and enter some text into the “Enter a Phrase” field, as shown below:

ns_task_Talking-first

Click Speak. Your computer should be speaking whatever you typed. No naughty words now, let’s keep this tutorial family friendly! :]

Handling GUI Interaction

Remember that last line of code you entered in speak:? The one that called waitUntilExit? Because of that line of code, if you keep clicking the Speak button while the computer is still talking, you’ll notice some unpleasant behavior.

Instead of speaking as soon as you click the button, your app queues up all of your clicks and repeats the phrase fully, once for each click. If you take a close look at your app window, you’ll notice that the Speak button stays blue while the app is speaking, as shown below:

nstask_blue_speak

The Speak button is still blue because your app is still processing the button click until the say task is complete. Click on the button a few times in rapid succession, and you’ll see the button flicker before each new utterance of the phrase. That’s because your app has just finished processing the previous button click, only to immediately handle the next queued up button press.

This isn’t just a problem with the Speak button. If you try interacting with the application at all, like clicking on any of the controls in either window, you will find a completely unresponsive GUI. Hmm — that doesn’t really offer a great user experience.

At first glance, it seems like there should be a quick fix for this.

Open AppDelegate.m, and comment out [task waitUntilExit], as shown in the code snippet below:

// Don't do anything else until the task finishes
// [task waitUntilExit];

Build and run your app again, enter some text into the text field of the Talking window, and click Speak three or four times in rapid succession.

Are the results what you expected? Since NSTask is no longer blocking your main thread, the button presses aren’t queued and your application will launch a separate speak: process each time you click the button. This causes the voices to overlap and makes it difficult to discern what your computer is saying.

Note: While each of these results seems bad in its own way, there are times where either approach might be appropriate. It really depends on things like whether or not your program needs the task’s output before continuing, how long the task generally takes to complete, and what the user might be able to do with your program while a task is running. You’ll have to decide for yourself based on your own situation.

There’s one last thing to try before you’re done with the Talking window. So far you’ve only passed a single argument to say. What if you wanted to use another voice, like Vicki?

To do this, you’ll need to modify your code to pass some flags and additional arguments to say.

Modify one line of speak: inside of AppDelegate.m as shown below:

task.arguments  = @[@"-v", @"vicki", speakingPhrase];

As before, you create an array of arguments that will be passed to the launched task. However, this time you are passing -v vicki in addition to the phrase to be spoken.

Build and run your application again, and type whatever you wish into the text field. You’ll now hear the computer speaking to you in Vicki’s voice, just as you did when using the command line arguments.

It’s important to recall that the items in the arguments array are passed to the application in order. That means if you need to pass a flag, you need to ensure it’s positioned directly before the argument to which it applies. For example, if you were to modify the content of task.arguments to look like this:

task.arguments  = @[@"-v", speakingPhrase, @"vicki"];

When you ran the application, you’d see the following errors in Xcode’s Debug area:

nstask_bad_args

Preparing the Defaults

Your second NSTask example will be to build and package an iOS apps into an ipa file by using NSTask by running some command line tools in the background. Most of the basic UI functionality is in place — your job is do the heavy lifting with NSTask!

Note: It’s recommended that you have an iOS Developer account with Apple, as you’ll need the proper certificates and provisioning profile to create an ipa file that can be submitted to Apple. However, you should still be able to follow along and create everything for the purposes of this tutorial without having to shell out $99.

To build and package your iOS apps into an ipa file, you will need to install the Xcode Command Line Tools.

To check if they’re installed already, go to Xcode\Preferences and click on the Downloads tab. You should see something like the following:

ns_task_xcodedownloads

If you see the “Installed” status for Command Line Tools, you’re good to go. If not, click Install and come back to this tutorial when it’s done installing. Once you’ve made sure that Command Line Tools are installed, continue forward.

You are now going to work on the window titled “TasksProject”. The first section in the window asks the user to select an Xcode project directory. Rather than having to select a directory manually every time you run this app as you’re testing, to save time you’re going to hard-code it to one of your own Xcode project directories.

To do this, head back to XCode and open AppDelegate.h. Take a look at the properties and methods under the comment “Project Package”:

@property (unsafe_unretained) IBOutlet NSTextView *outputText;
@property (weak) IBOutlet NSProgressIndicator *spinner;
@property (weak) IBOutlet NSPathControl *projectPath;
@property (weak) IBOutlet NSPathControl *repoPath;
@property (weak) IBOutlet NSButton *buildButton;
@property (weak) IBOutlet NSTextField *targetName;
- (IBAction)startTask:(id)sender;
- (IBAction)stopTask:(id)sender;

All of these properties and methods that you see correspond to the TasksProject window in MainMenu.xib. Notice the projectPath property – this is the one you want to change.

Open MainMenu.xib and click on the Project Location item of Window – TasksProject. In the Attributes Inspector, under Path Control, find the Path element, as shown in the following image:

nstask_project_loc

Set Path to a directory on your machine that contains an iOS project. Make sure you use the parent directory of a project, not the .xcodeproj file itself.

Note: Again, you’re only setting the path directly in your app to make testing easier. When you run the app, you’ll be able to choose any directory containing an iOS project on your computer, but the path you provide here will be the default selection. That way, you can quickly test your app without having to choose a project path every time you run.

If you don’t have any iOS projects on your machine, download a sample iOS app here and unzip it to a location on your machine. Then set the Path property in your application using the instructions above. For example, if you unzip the package on your desktop, you would set Path to
/Users/YOUR_USERNAME_HERE/Desktop/SuperDuperiPhoneApp
.

Now that you have a default source project path in your app to facilitate testing, you will probably want a default destination path for the same reason. Open MainMenu.xib, and click on the Build Repository item of Window – TasksProject.

In the Attributes Inspector, find Path item under Path Control as shown in the following screenshot:

nstask_build_repo3

Set the Path entry to a directory on your machine that’s easy to find, like the Desktop. This is where the .ipa file created by this application will be placed.

There are two additional fields in the TasksProject window that you’ll need to know about, as shown below:

nstask_tasksproject

  • The first field “Target Name” is designated for the name of the iOS Target you want to build. Don’t know the target name of your project? Check the note section below.
  • The second field is a text area that will display output from the NSTask object in your project as it runs.

To find the target name for your iOS project, select your iOS project in Xcode’s project navigator and look under TARGETS in the Info tab. The screenshot below shows where to find this for a sample project called “MyRss”:

ns_task_xcodeTargetExample

With the detail of the target name out of the way, you can start fleshing out the bits of code that will run when the “Build” button is pressed.

Preparing the Spinner

Open AppDelegate.m and add the following code to startTask:

//1
self.outputText.string = @"";
 
//2
NSString *projectLocation  = [self.projectPath.URL path];
NSString *finalLocation    = [self.repoPath.URL path];
 
//3
NSString *projectName      = [self.projectPath.URL lastPathComponent];
NSString *xcodeProjectFile = [projectLocation stringByAppendingString:[NSString stringWithFormat:@"/%@.xcodeproj", projectName]];
 
//4
NSString *buildLocation    = [projectLocation stringByAppendingString:@"/build/Release-iphoneos"];
 
// 5
NSMutableArray *arguments = [[NSMutableArray alloc] init];
[arguments addObject:xcodeProjectFile];
[arguments addObject:self.targetName.stringValue];
[arguments addObject:buildLocation];
[arguments addObject:projectName];
[arguments addObject:finalLocation];
 
// 6
[self.buildButton setEnabled:NO];
[self.spinner startAnimation:self];

Note: Xcode may display some warnings complaining about unused variables. Don’t worry, you aren’t done writing this method yet.

Here’s a step-by-step explanation of the code above:

  1. Clear the output text area each time the build process runs.
  2. Store the project directory and output directory in projectLocation and finalLocation respectively, using an NSURL.
  3. Define the location of the Xcode project file by concatenating lastPathComponent with the .xcodeproj extension.
  4. Define the subdirectory where your task will store intermediate build files while it’s creating the ipa file as ./build/Release-iphoneos
  5. Create an array of arguments from the variables in this method. This array will be passed to NSTask to be used when launching the command line tools to build your .ipa file.
  6. Finally, simply disable the “Build” button and start a spinner animation.

Why disable the “Build” button? Recall the problem with speak: in the previous application window, where the application either queued the button click events (making the app unresponsive) or reacted immediately to each button click (making it too responsive).

Disabling the “Build” button while the app is building is the first step to solving that user interface problem. This prevents the user from creating button click events while the app is busy.

The spinner animation is simply there to inform the user that the application is currently busy.

Build and run your application, and hit the Build button. You should now see the “Build” button disable and the spinner animation start, as shown in the following screenshot:

nstask_spinning

Your app looks pretty busy, but you know in actual fact it’s not really doing anything. Time to add some NSTask magic.

Adding an NSTask to TasksProject

Open AppDelegate.m and add the following method:

- (void)runScript:(NSArray*)arguments {
    //1
    dispatch_queue_t taskQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0);
    dispatch_async(taskQueue, ^{
 
        //2
        self.isRunning = YES;
 
        //3
        @try {
           [NSThread sleepForTimeInterval:2]; // THIS LINE FOR TESTING
        }
        //4
        @catch (NSException *exception) {
            NSLog(@"Problem Running Task: %@", [exception description]);
        }
        //5
        @finally {
            [self.buildButton setEnabled:YES];
            [self.spinner stopAnimation:self];
            self.isRunning = NO;
        }
    });
}

The above code completes the solution of the non-responsive / over-responsive app problem you faced in the first part of this tutorial. If you look at the method step-by-step, you’ll see that the code does the following:

  1. Uses dispatch_async to run on a background thread. The application will continue to process things like button clicks on the main thread, but the NSTask will run on the background thread until it is complete.
  2. Sets isRunning to YES. This enables the Stop button, since it’s bound to the AppDelegate‘s isRunning property via Cocoa Bindings.
  3. Wraps your task in a try/catch/finally block. This gives you a way to respond to problems rather than just crashing your application. For now, the @try portion just has a temporary line of code that causes the current thread to sleep for 2 seconds, simulating a long-running task.
  4. Logs an error in the @catch section. This is OK for testing, but in a production-level application, you will want to alert the user and give them an opportunity to remedy the issue.
  5. Resets the UI details in the @finally section. Here you re-enable the Build button, stop the spinner animation, and set isRunning to NO which disables the “Stop” button.

Now that you have a method that will run your task in a separate thread, you need to call it from somewhere in your app.

Still in AppDelegate.m, add the following code to the very end of startTask::

[self runScript:arguments];

This simply calls runScript: with the array of arguments you built in startTask:.

Build and run your application and hit the Build button. You’ll notice that the Build button will become disabled, the Stop button will become enabled, and the spinner will start animating, as shown in the screenshot below:

Screen Shot 2013-07-17 at 3.42.56 PM

While the spinner is animating, you will still be able to interact with the application. Try it yourself — for example, you should be able to type in the Target Name field while the spinner is active.

After two seconds have elapsed, the spinner will disappear, Stop will become disabled, and Build will become enabled.

Note:If you have trouble interacting with the application before it’s done sleeping, just increase the number of seconds in your call to sleepForTimeInterval:

Okay, now that you’ve solved the UI responsiveness issues, you can finally implement your call to NSTask.

In AppDelegate.m, find the line in runScript: that ends with the comment // THIS LINE FOR TESTING. Replace that entire line of code with the following:

            //1
            NSString *path  = [NSString stringWithFormat:@"%@", [[NSBundle mainBundle] pathForResource:@"BuildScript" ofType:@"command"]];
 
            //2
            self.buildTask            = [[NSTask alloc] init];
            self.buildTask.launchPath = path;
            self.buildTask.arguments  = arguments;
 
            // TODO: Output Handling
 
            //3
            [self.buildTask launch];
 
            //4
            [self.buildTask waitUntilExit];

The above code should look pretty familiar — it’s very similar to what you did in speak:.

Nevertheless, here’s a comment-by-comment explanation of the above code:

  1. Get the path to a script named BuildScript.command included in your application’s bundle. That script doesn’t exist right now — you’ll be adding it shortly.
  2. Create a new NSTask object and assign it to the AppDelegate‘s buildTask property. Then assign the BuildScript.command‘s path to the NSTask‘s launchPath. As well, assign the arguments that were passed to runScript:to NSTask‘s arguments property.
  3. Call launch on the NSTask object, which will run the BuildScript.command script.
  4. Call waitUntilExit which tells the NSTask object to block any further activity on the current thread until the task is complete. Remember — this code is running on a background thread so your UI, which is running on the main thread, will still respond to user input.

Build and run your project; you won’t notice that anything looks different, but hit the Build button and check the output console. You should see an error like the following:

TasksProject[46604:1803] Problem Running Task: launch path not accessible

This is the log from the try/catch block you added to runScript:. NSTask throws an error since it can’t find the Build.command script.

Looks like it’s time to write that script! :]

Writing a Build Shell Script

In Xcode, choose File\New\File… and select the Other category under OS X. Choose Shell Script and hit Next, as shown below:

nstask_new_script

Name the file BuildScript.command. Before you hit Create, be sure TasksProject is selected under Targets, as shown in the screenshot below:

nstask_new_script2

Open BuildScript.command, and add the following commands at the end of the file:

echo "*********************************"
echo "Build Started"
echo "*********************************"
 
echo "*********************************"
echo "Beginning Build Process"
echo "*********************************"
xcodebuild -project "${1}" -target "${2}" -sdk iphoneos -verbose
 
echo "*********************************"
echo "Creating IPA"
echo "*********************************"
/usr/bin/xcrun -verbose -sdk iphoneos PackageApplication -v "${3}/${4}.app" -o "${5}/app.ipa"

This is the entire build script that your NSTask calls.

The echo commands that you see throughout your script will send whatever text is passed to them to standard output, which you capture as part of the return values from your NSTask object and display in your outputText field. echo statments are handy statements to let you know what your script is doing, since many commands don’t provide much output, or any at all, when run from the command line.

You’ll notice that besides all of the echo commands, there are two other commands: xcodebuild, and xcrun.

xcodebuild builds your application, creates a .app file, and places it in the subdirectory /build/Release-iphoneos. Recall that you created an argument that references this directory way back in startTask:, since you needed a place for the intermediate build files to live during the build and packaging process.

xcrun runs the developer tools from the command line. Here you use it to call PackageApplication, which packages the .app file into an .ipa file. By setting the verbose flag, you’ll get a lot of details in the standard output, which you’ll be able to view in your outputText field.

In both the xcodebuild and xcrun commands, you’ll notice that all of the arguments are written “${1}” instead of$1. This is because the paths to your projects may contain spaces. To handle that condition, you must wrap your file paths in quotes in order to get the right location. By putting the paths in quotes and curly braces, the script will properly parse the full path, spaces and all.

What about the other parts of the script — the ones that Xcode automatically added for you. What do they mean?

The first line of the script looks like this:

#!/bin/sh

Although it looks like a comment since it’s prefixed with #, this line tells the operating system to use a specific shell when executing the remainder of the script. The shell is the interpreter that runs your commands, either in script files, or from a command line interface.

There are many different shells available, but most of them adhere to some variation of either Bourne shell syntax or C shell syntax. Your script indicates that it should use sh, which is one of the shells included with OS X.

If you wanted to specify another shell to execute your script, like bash, you would change the first line to contain the full path to the appropriate shell executable, like so:

#!/bin/bash

In scripts, any argument you pass in is accessed by a $ and a number. $0 represents the name of the program you called, with all arguments after that referenced by $1, $2, and so forth.

Note:Shell scripts have been around for about as long as computers, so you’ll find more information than you’ll ever want to know about them on the Internet. For a simple (and relevant) place to start, check out Apple’s Shell Scripting Primer.

Okay, so you have your app and your script. You’re ready to start calling your script from NSTask, right?

Not quite. At this point, your script file doesn’t have execute permissions. That is, you can read and write the file, but you can’t execute it.

To make it executable, navigate to your project directory in Terminal. Terminal defaults to your Home directory, so if your project is in your Documents directory, you would type the command:

cd Documents/TasksProject

If your project is in another directory besides “Documents/TasksProject”, then you’ll need to enter the correct path to your project folder. To do this quickly, click and drag your project folder from the Finder into Terminal. The path to your project will magically appear in the Terminal window! Now simply move your cursor to the front of that path,type cd followed by a space, and hit enter.

To make sure you’re in the right place, type the following command into Terminal:

ls

Check that BuildScript.command in the file listing produced. If not, check that you’ve correctly entered your project directory in Terminal.

Once you’re assured that you’re in the correct directory, type the following command into Terminal:

chmod +x BuildScript.command

The chmod command changes the permissions of the script to allow it to be executed by your NSTask object. If you tried to run your application without these permissions in place, you’d see the same “Launch path not accessible” error as before.

Clean and run your project; the “clean” is necessary as Xcode won’t pick up on the file’s permissions changing, and therefore won’t copy it into the build repository. Once the application opens up, type in the target name of your test app, ensure the “Project Location” and “Build Repository” values are set correctly, and finally hit Build.

When the spinner disappears, you should have a new .ipa file in your desired location. Success!

Using Outputs

Okay, you’re pretty well versed in passing arguments to command line programs, but what about dealing with the output of these command line programs?

To see this in action, type the following command into Terminal and hit Enter:

date

You should see a message produced that looks something like this:

Sun May 12 22:07:04 EDT 2013

date tells you the current date and time. Although it isn’t immediately obvious, the results were sent back to you on a channel called standard output.

Processes generally have three default channels of input and output:

  • standard input, which accepts input from the caller,
  • standard output, which sends output from the process back to the caller, and
  • standard error, which sends errors from the process back to the caller.

Protip: you’ll see these commonly abbreviated as stdin, stdout, and stderr.

There is also a pipe that allows you to redirect the output of one process into the input of another process. You’ll be creating a pipe to let your application see the standard output from the process that NSTask runs.

However, to see a pipe in action, give it a try first in Terminal.

Ensure the volume is turned up on your computer, and type the following command in Terminal:

date | say

Hit enter and you should hear your computer telling you what time it is.

Note: The pipe character “|” on your keyboard is usually located on the forward slash \ key, just above the enter/return key.

Here’s what just happened: you created a pipe that takes the standard output of date and redirects it into the standard input of say. And yes, you can still provide options to the commands that communicate with pipes, so if you’re missing the sultry tones of Vicki’s voice, type the following command instead:

date | say -v vicki

You can construct some rather long chains of commands using pipes, redirecting the stdout from one command into the stdin of another. Once you get comfortable using stdin, stdout, and pipe redirects, you can do some really complicated things from the command line! using tools like pipes.

Time to make good on that promise to implement a pipe in your app.

Open AppDelegate.m and replace the comment that reads // TODO: Output Handling in runScript: with the following code:

// Output Handling
//1
self.outputPipe               = [[NSPipe alloc] init];
self.buildTask.standardOutput = self.outputPipe;
 
//2
[[self.outputPipe fileHandleForReading] waitForDataInBackgroundAndNotify];
 
//3
[[NSNotificationCenter defaultCenter] addObserverForName:NSFileHandleDataAvailableNotification object:[self.outputPipe fileHandleForReading] queue:nil usingBlock:^(NSNotification *notification){
    //4
    NSData *output = [[self.outputPipe fileHandleForReading] availableData];
    NSString *outStr = [[NSString alloc] initWithData:output encoding:NSUTF8StringEncoding];
    //5
    dispatch_sync(dispatch_get_main_queue(), ^{
        self.outputText.string = [self.outputText.string stringByAppendingString:[NSString stringWithFormat:@"\n%@", outStr]];
	// Scroll to end of outputText field
        NSRange range;
        range = NSMakeRange([self.outputText.string length], 0);
        [self.outputText scrollRangeToVisible:range];
    });
    //6
    [[self.outputPipe fileHandleForReading] waitForDataInBackgroundAndNotify];
}];

This block of code collects the output from the external process and adds it to the GUI’s outputText field. It works as follows:

  1. Create an NSPipe and attach it to buildTask‘s standard output. NSPipe is a class representing the same kind of pipe that you created in Terminal. Anything that is written to buildTask‘s stdout will be provided to this NSPipe object.
  2. Call fileHandleForReading to get a reference to the location where the pipe dumps its output. You then call waitForDataInBackgroundAndNotify on that object to use a separate background thread to check for available data.
  3. Whenever data is available, waitForDataInBackgroundAndNotify will notify you by calling the block of code you register with NSNotificationCenter to handle NSFileHandleDataAvailableNotification.
  4. Inside your notification handler, get the data as an NSData object and convert it to a string.
  5. On the main thread, append the string from the previous step to the end of the text in outputText and scroll the text area so that the user can see the latest output as it arrives. This must be on the main thread as all GUI redraws and user interactions occur on that thread, and you need to make sure you aren’t trying to modify the GUI while one of those things is happening.
  6. Finally, repeat the call to wait for data in the background. This creates a loop that will continually wait for available data, process that data, wait for available data, and so on.

Build and run your application again; make sure the Project Location and Build Repository fields are set correctly, type in your target’s name, and click Build.

You should see the output from the building process in your outputText field, as shown in the screenshot below:

nstask_showing_output

Stopping an NSTask

What happens if you start a build and then change your mind? What if it’s taking too long, or something else seems to have gone wrong and it’s just hanging there, making no progress? These are times when you’ll want to be able to stop your background task. Fortunately, this is pretty easy to do.

In AppDelegate.m, add the following code to stopTask:

    if ([self.buildTask isRunning]) {
        [self.buildTask terminate];
    }

The code above simply checks if the NSTask is running, and if so, calls its terminate method. This will stop the NSTask in its tracks. Pretty simple, eh?

Build and run your app, ensure all fields are configured correctly, and hit the Build button. Then hit the Stop button before the build is complete. You’ll see that everything stops, and no new .ipa file is created in your output directory.

Where To Go From Here?

Here is the finished NSTask example project from the above tutorial.

Congratulations, you’ve just begun your process of becoming an NSTask ninja!

In one short tutorial, you’ve learned:

  • How to use Terminal to execute command line programs and pass arguments to those programs
  • How to create NSTasks with arguments and output pipes
  • How to create a shell script and call it from your app!

To learn more about NSTask, check out Apple’s official NSTask Class Reference.

Also, this tutorial only dealt with working with stdout with NSTask – you can use stdin and stderr as well! A good practice exercise would be to extend this tutorial to work with these.

I hope you enjoyed this NSTask tutorial and that you find it useful in your future Mac OSX apps. If you have any questions or comments, please join the forum discussion below!

Andy Pereira
Andy Pereira

Andy is a software developer in Atlanta, GA. He is the author of a few personal iOS apps, as well as several B2B mobile apps.

You can find Andy on LinkedIn or Twitter.

User Comments

2 Comments

  • What happened to the CSS on this site? The Font is horrible and difficult to read.
    PhillyNJ
  • Really good tutorial / walkthrough, easy to follow along, and the code supplied actually works. really helped me understand NSTask, the first example is great for me the beginner, but i came back and followed the creation of the IPA file with the output pipe in real time, its still hard to get my head around but your tutorial has sent me on a good road :), p.s. post some more articles like this :-)
    piresl

Other Items of Interest

Ray's Monthly Newsletter

Sign up to receive a monthly newsletter with my favorite dev links, and receive a free epic-length tutorial as a bonus!

Advertise with Us!

Hang Out With Us!

Every month, we have a free live Tech Talk - come hang out with us!


Coming up in September: iOS 8 App Extensions!

Sign Up - September

RWDevCon Conference?

We are considering having an official raywenderlich.com conference called RWDevCon in DC in early 2015.

The conference would be focused on high quality Swift/iOS 8 technical content, and connecting as a community.

Would this be something you'd be interested in?

    Loading ... Loading ...

Our Books

Our Team

Tutorial Team

  • Kyle Richter

... 49 total!

Update Team

Editorial Team

... 23 total!

Code Team

  • Orta Therox

... 1 total!

Translation Team

  • Victor Grushevskiy
  • Jiyeon Seo

... 33 total!

Subject Matter Experts

  • Richard Casey

... 4 total!