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
Preparing the Spinner
Open TasksViewController.swift and add the following code to startTask(_:)
:
//1.
outputText.string = ""
if let projectURL = projectPath.url, let repositoryURL = repoPath.url {
//2.
let projectLocation = projectURL.path
let finalLocation = repositoryURL.path
//3.
let projectName = projectURL.lastPathComponent
let xcodeProjectFile = projectLocation + "/\(projectName).xcodeproj"
//4.
let buildLocation = projectLocation + "/build"
//5.
var arguments:[String] = []
arguments.append(xcodeProjectFile)
arguments.append(targetName.stringValue)
arguments.append(buildLocation)
arguments.append(projectName)
arguments.append(finalLocation)
//6.
buildButton.isEnabled = false
spinner.startAnimation(self)
}
Here’s a step-by-step explanation of the code above:
-
outputText
is the large text box in the window; it will contain all the output from the script that you will be running. If you run the script multiple times, you’ll want to clear it out between each run, so this first line sets thestring
property (contents of the text box) to an empty string. - The
projectURL
andrepositoryURL
objects areNSURL
objects, and this gets the string representations of these objects in order to pass them as arguments to yourNSTask
. - By convention, the name of the folder and the name of the project file are the same. Getting the
lastPathComponent
property of the project folder contained inprojectURL
and adding an “.xcodeproj” extension gets the path to the project file. - Defines the subdirectory where your task will store intermediate build files while it’s creating the
ipa
file asbuild
. - Stores the arguments in an array. This array will be passed to
NSTask
to be used when launching the command line tools to build your.ipa
file. - Disables the “Build” button and starts a spinner animation.
Why disable the “Build” button? The NSTask
will run each time the button is pressed, and as the app will be busy for an amount of time while the NSTask
does its work, the user could impatiently press it many times — each time spawning a new build process. This action prevents the user from creating button click events while the app is busy.
Build and run your application, then hit the Build button. You should see the “Build” button disable and the spinner animation start:
Your app looks pretty busy, but you know right now it’s not really doing anything. Time to add some NSTask
magic.
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:)
.
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.
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! :]