NSTask Tutorial for OS X
In this OS X NSTask tutorial, learn how to execute another program on your machine as a subprocess and monitor its execution state while your main program continues to run. By Warren Burton.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Adding an NSTask to TasksProject
Open TasksViewController.swift and add the following method:
func runScript(_ arguments:[String]) {
//1.
isRunning = true
//2.
let taskQueue = DispatchQueue.global(qos: DispatchQoS.QoSClass.background)
//3.
taskQueue.async {
//TESTING CODE
//4.
Thread.sleep(forTimeInterval: 2.0)
//5.
DispatchQueue.main.async(execute: {
self.buildButton.isEnabled = true
self.spinner.stopAnimation(self)
self.isRunning = false
})
//TESTING CODE
}
}
If you look at the method step-by-step, you’ll see that the code does the following:
- Sets
isRunning
totrue
. This enables theStop
button, since it’s bound to theTasksViewController
‘sisRunning
property via Cocoa Bindings. You want this to happen on the main thread. - Creates a
DispatchQueue
to run the heavy lifting on a background thread. - Uses
async
on theDispatchQueue
The application will continue to process things like button clicks on the main thread, but theNSTask
will run on the background thread until it is complete. - This is a temporary line of code that causes the current thread to sleep for 2 seconds, simulating a long-running task.
- Once the job has finished, re-enables the
Build
button, stops the spinner animation, and setsisRunning
tofalse
which disables the “Stop” button. This needs to be done in the main thread, as you are manipulating UI elements.
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 TasksViewController.swift, add the following code to the end of startTask
just after spinner.startAnimation(self)
:
runScript(arguments)
This 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:
While the spinner is animating, you’ll 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, increase the number of seconds in your call to sleep(forTimeInterval:)
.
Now that you’ve solved the UI responsiveness issues, you can finally implement your call to NSTask.
Note: Swift calls the NSTask
class by the name Process
because of the Foundation framework stripping of the NS prefix in Swift 3. However you’ll read NSTask in this tutorial as thats going to be the most useful search term if you want to learn more.
In TasksViewController.swift, find the lines in runScript
that are bracketed by the comment //TESTING CODE
. Replace that entire section of code inside the taskQueue.async
block with the following:
//1.
guard let path = Bundle.main.path(forResource: "BuildScript",ofType:"command") else {
print("Unable to locate BuildScript.command")
return
}
//2.
self.buildTask = Process()
self.buildTask.launchPath = path
self.buildTask.arguments = arguments
//3.
self.buildTask.terminationHandler = {
task in
DispatchQueue.main.async(execute: {
self.buildButton.isEnabled = true
self.spinner.stopAnimation(self)
self.isRunning = false
})
}
//TODO - Output Handling
//4.
self.buildTask.launch()
//5.
self.buildTask.waitUntilExit()
The above code:
- Gets 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. - Creates a new
Process
object and assigns it to theTasksViewController
‘sbuildTask
property. ThelaunchPath
property is the path to the executable you want to run. Assigns theBuildScript.command
‘spath
to theProcess
‘slaunchPath
, then assigns the arguments that were passed torunScript:
toProcess
‘sarguments
property.Process
will pass the arguments to the executable, as though you had typed them into terminal. -
Process
has aterminationHandler
property that contains a block which is executed when the task is finished. This updates the UI to reflect that finished status as you did before. - In order to run the task and execute the script, calls
launch
on theProcess
object. There are also methods to terminate, interrupt, suspend or resume anProcess
. - Calls
waitUntilExit
, which tells theProcess
object to block any further activity on the current thread until the task is complete. Remember, this code is running on a background thread. 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:
Unable to locate BuildScript.command
This is the log from the guard statement at the start of the code you just added. Since you haven’t added the script yet, the guard
is triggered.
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:
Name the file BuildScript.command. Before you hit Create, be sure TasksProject is selected under Targets, as shown below:
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 CONFIGURATION_BUILD_DIR="${3}"
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, if any, output 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 an .app
file, and places it in the subdirectory /build. 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 parts 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. It’s called a shebang. 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.
Now 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.
This means if you build and run right now, your app will crash when you hit the Build button. Try it if you like. It’s not a big deal while developing, and you should see the exception “launch path not accessible” in your Xcode console.
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”, 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 you’re not in the right place, 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 try to run your application without these permissions in place, you’d see the same “Launch path not accessible” error as before. You only need to do this once for each new script that you add to your project.
Note: Using scripts like this is simple if you are developing for yourself or shipping your app outside the Mac App Store (MAS), however when developing for the MAS the sandbox rules that apply to your app are inherited by your scripts and you’ll need to use more complex techniques to use command line programs. These techniques are beyond the scope of this tutorial. See the links at the end for more details.
Clean and run your project; the “clean” is necessary as Xcode won’t pick up on the file’s permissions change, 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!