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? UseSpotlight — 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:
Click the Terminal application in the results list, and you’ll be presented with a screen that looks like this:
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:
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:
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:
|
The above code does the following:
- Creates a new
NSTask
object. This object lets you instantiate and control external applications. - Tells the
NSTask
object which program to run. You need to provide the complete path to the file so theNSTask
can find it. In this case, thesay
program lives in your Mac’s /usr/bin/ directory. - 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. - Calls
launch
on theNSTask
object to execute the program it was instructed to run. - Calls
waitUntilExit
. This method begins monitoring theNSTask
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:
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 Speakbutton stays blue while the app is speaking, as shown below:
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:
|
Build and run your app again, enter some text into the text field of the Talking window, and click Speakthree 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:
|
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:
|
When you ran the application, you’d see the following errors in Xcode’s Debug area:
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:
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”:
|
All of these properties and methods that you see correspond to the TasksProject window inMainMenu.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:
Set Path to a directory on your machine that contains an iOS project. Make sure you use the parentdirectory 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 Repositoryitem of Window – TasksProject.
In the Attributes Inspector, find Path item under Path Control as shown in the following screenshot:
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:
- 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”:
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:
|
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:
- Clear the output text area each time the build process runs.
- Store the project directory and output directory in
projectLocation
andfinalLocation
respectively, using anNSURL
. - Define the location of the Xcode project file by concatenating
lastPathComponent
with the.xcodeproj
extension. - Define the subdirectory where your task will store intermediate build files while it’s creating the
ipa
file as./build/Release-iphoneos
- 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. - 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:
Your app looks pretty busy, but you know in actual fact it’s not really doing anything. Time to add someNSTask
magic.
Adding an NSTask to TasksProject
Open AppDelegate.m and add the following method:
|
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:
- Uses
dispatch_async
to run on a background thread. 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. - Sets
isRunning
toYES
. This enables theStop
button, since it’s bound to theAppDelegate
‘sisRunning
property via Cocoa Bindings. - 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. - 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. - Resets the UI details in the
@finally
section. Here you re-enable theBuild
button, stop the spinner animation, and setisRunning
toNO
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:
:
|
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:
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:
|
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:
- 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. - Create a new
NSTask
object and assign it to theAppDelegate
‘sbuildTask
property. Then assign theBuildScript.command
‘spath
to theNSTask
‘slaunchPath
. As well, assign the arguments that were passed torunScript:
toNSTask
‘sarguments
property. - Call
launch
on theNSTask
object, which will run theBuildScript.command
script. - Call
waitUntilExit
which tells theNSTask
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:
|
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 hitNext, as shown below:
Name the file BuildScript.command. Before you hit Create, be sure TasksProject is selected underTargets, as shown in the screenshot below:
Open BuildScript.command, and add the following commands at the end of the file:
|
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 tostandard 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
, andxcrun
.
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:
|
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:
|
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:
|
This block of code collects the output from the external process and adds it to the GUI’s outputText
field. It works as follows:
- Create an
NSPipe
and attach it tobuildTask
‘s standard output.NSPipe
is a class representing the same kind of pipe that you created inTerminal
. Anything that is written tobuildTask
‘sstdout
will be provided to thisNSPipe
object. - Call
fileHandleForReading
to get a reference to the location where the pipe dumps its output. You then callwaitForDataInBackgroundAndNotify
on that object to use a separate background thread to check for available data. - Whenever data is available,
waitForDataInBackgroundAndNotify
will notify you by calling the block of code you register withNSNotificationCenter
to handleNSFileHandleDataAvailableNotification
. - Inside your notification handler, get the data as an
NSData
object and convert it to a string. - 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. - 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:
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:
|
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 theStop 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!
原文地址:http://www.raywenderlich.com/36537/nstask-tutorial
相关资料:https://pythonhosted.org/pyobjc/examples/index.html