Scripting Bridge is a new technology that lets you control scriptable Apple and third-party applications using standard Objective-C syntax.
In the past, when you wanted your Cocoa application to control other scriptable applications, you had two main choices: you could write an AppleScript script to do the job and use Cocoa's NSAppleScript
class to execute the script, or you could use NSAppleEventDescriptor
to manually send and receive Apple events.
New in Leopard, the Scripting Bridge provides a straightforward model for control of applications, is up to 100 times faster than the traditional NSAppleScript
solution and doesn't require detailed knowledge of the target application's Apple event model. In addition to directly controlling one Cocoa application from another, the Scripting Bridge provides a foundation for Ruby (via RubyCocoa) and Python (via PyObjC) to control applications, giving those scripting languages the same advantages enjoyed by AppleScript.
The Benefits of Scripting Bridge
How Scripting Bridge Works
Lazy Evaluation
Automatic Application Launch
Filtering Arrays
With Scripting Bridge, you can automatically build "glue" code that lets you access a particular scriptable application. You include the code in your application, then make standard Objective-C calls to perform the desired operations. If you are working with iTunes, for example, you can get the name of the current iTunes track with a simple line of code:
NSString *currentTrackName = [[iTunes currentTrack] name];
In addition to letting you work with Objective-C syntax, Scripting Bridge has these advantages:
It uses native Cocoa data types—NSString
, NSArray
, NSURL
, and so on—so you'll never have to deal with less familiar Carbon types.
It uses standard Cocoa memory management, so you don't have to manually allocate and free space for Apple events.
It requires an order of magnitude less code than using NSAppleEventDescriptor
or direct calls.
It checks for syntax errors at compile time, unlike NSAppleScript
.
It gives your application access to any scriptable feature of any available scriptable application.
It runs more than twice as fast as a precompiled NSAppleScript
, and up to 100x as fast as an uncompiled NSAppleScript
.
It integrates seamlessly with existing Objective-C code.
Scripting Bridge is a system framework included with Mac OS X version 10.5 and later. It defines the following classes: SBApplication
(an application your application can communicate with), SBObject
(an object within an application—for example, an iTunes track), and SBElementArray
(a collection of SBObject
instances—for example, a collection of all tracks in iTunes)
With the exception of SBElementArray
, you rarely deal with these classes directly. Instead, you deal with subclasses in the generated "glue" code. If you were controlling iTunes, for example, you'd use classes such as iTunesApplication
(which inherits from SBApplication
) and iTunesTrack
(which inherits from SBObject
).
However, it is important to note that the Scripting Bridge framework itself does not include these application-specific subclasses, but instead creates them at run time.
To use Scripting Bridge, you use the sdp
tool to generate headers based on the AppleScript dictionary of the application you want to control. For example, the following figure shows the dictionary for iTunes:
The corresponding header file generated by sdp
would thus look like this:
@interface iTunesApplication : SBApplication { |
} |
... |
- (void) fastForward; // skip forward in a playing track |
- (void) nextTrack; // advance to the next track in the current playlist |
- (void) pause; // pause playback |
... |
@end |
To generate this glue code, you invoke the sdef and sdp tools in a command with the following format:
sdef
pathToApplication | sdp -fh --basename
applicationName
Note: There is both an sdef
file format for scripting definitions, available since Mac OS X version 10.2, and an sdef
tool, first available in Mac OS X v 10.5 (Leopard).
This command line uses sdef
to get the scripting definition from the application; if the application does not contain an actual sdef
, but does contain scripting information in either of the older 'aete' or Cocoa script suite formats, the tool translates that information into the sdef
format. The command pipes the output of the sdef
tool to sdp
to generate the corresponding header file. Here is a full command line for iTunes:
sdef /Applications/iTunes.app | sdp -fh --basename iTunes |
This command will produce a file iTunes.h
in the current directory. Add this file to your Xcode project. Then, link your project with the Scripting Bridge framework: choose Add to Project from the Project menu, and navigate to /System/Library/Frameworks/ScriptingBridge.framework.
Notice that you have only created a header for all the iTunes classes; your application does not contain implementations for any of them, because Scripting Bridge will create them on the fly. To start communicating with iTunes, you must first tell Scripting Bridge to create the application class:
iTunesApplication *iTunes = [SBApplication applicationWithBundleIdentifier:@"com.apple.iTunes"]; |
This locates the application by its bundle identifier, reads its scripting dictionary, and creates Objective-C classes for all the classes defined there. It also creates an instance representing the application; you can communicate with iTunes by sending messages to the application object.
Note: You can generate similar glue code for TextEdit, Safari, iChat, all of the iLife applications, Adobe Photoshop, Microsoft Word, and many other popular applications. In fact, the only requirement for creating headers to control an application is that it the application have an AppleScript dictionary. (The caveat, of course, is that applications with poorly designed AppleScript dictionaries will have less useful Cocoa headers as well.)
When sdp
generates header files, it automatically adds a comment to each method declaration, taken from the corresponding term in the application's AppleScript dictionary. For example, in the header file generated for the Finder, you'll find the following declaration in the FinderApplication class:
- (void)empty; // Empty the trash |
Application-specific classes make a point of returning data in a form that's useful to you. For example, the Finder's startupDisk
method returns a FinderDisk object, and iTunes' currentTrack
method returns an iTunesTrack object. When you ask an object for its name using the name
method, you get the result back as an instance of NSString
. Similarly, you get and set properties of an application the same way you do for instance variables inside your application: using methods such as ignoresPrivileges
and setIgnoresPrivileges:
. All of these features make it easier to incorporate Scripting Bridge code into your existing projects.
Like AppleScript, Scripting Bridge uses Apple events to send and receive information from other applications. However, because sending Apple events can be expensive, Scripting Bridge is designed to avoid sending Apple events until absolutely necessary.
Scripting Bridge accomplishes this by the use of references. When you ask for an object from an application, what you actually get back is a reference to it; Scripting Bridge won't evaluate the reference until you need some concrete data from it. For example, Scripting Bridge won't send an Apple event when you ask for the first disk of the Finder, but it will send an event when you ask for the name of the first disk of the Finder. You can see this in action in the following code:
// Get the shared FinderApplication instance |
FinderApplication *finder = [SBApplication applicationWithBundleIdentifier:@"com.apple.finder"]; |
// Get a reference which represents every disk of the Finder (doesn't send an Apple event) |
SBElementArray *disks = [finder disks]; |
// Get a reference to the first disk of the Finder (doesn't send an Apple event) |
FinderDisk *firstDisk = [disks objectAtIndex:0]; |
// Evaluate the firstDisk reference by sending it an Apple Event requesting its name |
NSString *name = [firstDisk name]; |
NSLog(name); // Log the name of the first disk |
Most of the time, the fact that Scripting Bridge is lazy about evaluating statements won't make much difference to you. However, there are times when you have to be careful to ensure that you get the behavior you expect.
Say you were faced with the following code:
// Get the shared iTunes instance |
iTunesApplication *iTunes = [SBApplication applicationWithBundleIdentifier:@"com.apple.iTunes"]; |
// Get the current track |
iTunesTrack *current = [iTunes currentTrack]; |
// Wait for 10 minutes |
sleep(600); |
//Print out the name of the current track |
NSLog([current name]); |
At first glance, it might appear that this code will log the name of whatever track was playing 10 minutes ago when it gets to the bottom line. In fact, what will happen is that the code will log the name of whatever track is currently playing.
Why? Recall that Scripting Bridge deals merely with references to objects until you actually need some concrete data from them. So what current
stores is a reference to whatever track is currently playing. This reference actually gets evaluated only when you call the name
method, which happens 10 minutes later.
But what if that's not what you want? What if you want to force the current reference to be evaluated as soon as it is created?
For that, you use the get
method, which is declared by SBObject
. In essence, the get
method tells Scripting Bridge, "stop being lazy—I want you to evaluate this object now." The following code shows how to change result of the previous example by using get
:
// Get the shared iTunes instance |
iTunesApplication *iTunes = [SBApplication applicationWithBundleIdentifier:@"com.apple.iTunes"]; |
// Get a reference to the current track and force it to be evaluated |
iTunesTrack *theTrack = [[iTunes currentTrack] get]; |
// Wait for 10 minutes |
sleep(600); |
//Print out the name of the track that was playing 10 minutes ago |
NSLog([theTrack name]); |
Because this code uses the get
method, theTrack
will always hold a reference to the track that was playing when the get
method executed.
Although the lazy nature of Scripting Bridge may not always be what you want, it dramatically reduces the number of Apple events that need to be sent. This allows your application to run significantly faster.
If you're not careful about how you write your code, however, you may end up entirely bypassing Scripting Bridge's laziness, forcing it to send more Apple events than necessary. Here are several common errors
Using the get
method when it isn't needed. Scripting Bridge is excellent at determining when it needs to send an Apple event to get information you want. When you write [someObject name]
, for example, Scripting Bridge automatically sends an Apple event to determine the object's name. If you instead write [[someObject get] name]
, you force Scripting Bridge to send two messages instead of one.
Sending the same message multiple times. Every time you ask an object for a concrete property—such as its name—you force an Apple event to be sent. Thus, in the following example, up to three Apple events may be sent:
FinderApplication *finder = [SBApplication applicationWithBundleIdentifier:@"com.apple.finder"]; |
SBElementArray *disks = [finder disks]; |
if ([[[disks objectAtIndex:0] name] isEqualToString:@"Macintosh HD"]) { |
//First Apple event sent |
NSLog(@"The first disk's name is Macintosh HD"); |
} else if ([[[disks objectAtIndex:0] name] isEqualToString:@"Disk 1"]) { |
//If execution reaches here, second Apple event sent |
NSLog(@"The first disk's name is Disk 1"); |
} else if ([[[disks objectAtIndex:0] name] isEqualToString:@"My Disk"]) { |
//If execution reaches here, third Apple event sent |
NSLog(@"The first disk's name is My Disk"); |
} |
To guarantee that only a single Apple event gets sent, make sure the name
method is called only once:
FinderApplication *finder = [SBApplication applicationWithBundleIdentifier:@"com.apple.finder"]; |
SBElementArray *disks = [finder disks]; |
NSString *name = [[disks objectAtIndex:0] name]; //Single Apple event sent |
if ([name isEqualToString:@"Macintosh HD"]) { //No additional event sent |
NSLog(@"The first disk's name is Macintosh HD"); |
} else if ([name isEqualToString:@"Disk 1"]) { //No additional event sent |
NSLog(@"The first disk's name is Disk 1"); |
} else if ([name isEqualToString:@"My Disk"]) { //No additional event sent |
NSLog(@"The first disk's name is My Disk"); |
Manually iterating through arrays. SBElementArray
objects are lazy as well; they don't send any Apple events until absolutely necessary. When you manually iterate through an SBElementArray
object, however, you force Scripting Bridge to send as many Apple events as there are items in your array. Take the following example, which makes a list of the name of every disk in the Finder:
FinderApplication *finder = [SBApplication applicationWithBundleIdentifier:@"com.apple.finder"]; // 1 |
SBElementArray *disks = [finder disks]; // 2 |
NSEnumerator *e = [disks objectEnumerator]; // 3 |
FinderDisk *currentDisk; // 4 |
NSMutableArray *nameArray = [NSMutableArray arrayWithCapacity:[disks count]]; // 5 |
while ((currentDisk = [e nextObject]) != nil) { // 6 |
[nameArray addObject:[currentDisk name]]; // 7 |
} |
NSLog(@"%@", nameArray); |
This code is extremely inefficient. First, Scripting Bridge must send an Apple event at line 5 to count the number of disks. Then, each time through the while
loop, it must send an additional Apple event to get the name of the current disk.
To avoid this inefficiency, SBElementArray
provides an optimized arrayByApplyingSelector:
method, which is guaranteed to send no more than one Apple event. That allows you to rewrite the previous sample as follows:
FinderApplication *finder = [SBApplication applicationWithBundleIdentifier:@"com.apple.finder"]; |
SBElementArray *disks = [finder disks]; |
NSArray *nameArray = [disks arrayByApplyingSelector:@selector(name)]; |
NSLog(@"%@", nameArray); |
This code sends only a single Apple event.
If an application isn't open when Scripting Bridge tries to send it an Apple event, Scripting Bridge will automatically launch it—which may come as a surprise to the users of your application. In addition, while the other application is launching, your application's execution will be blocked.
For these reasons, it is often a good idea to check whether an application is running before you try to communicate with it. Suppose you want to write a method that gets the name of the current track, but only if iTunes is running. You could do so with code like the following:
- (NSString *) nameOfCurrentTrack |
{ |
if ([iTunes isRunning]) { // Checks to see if iTunes is running BEFORE getting the name of the current track |
return [[iTunes currentTrack] name]; |
} |
return nil; |
} |
The isRunning
method is defined for all instances of SBApplication
.
AppleScript has a particularly powerful operator—whose
—that can filter a list of objects at once. For example, you could say:
tell application "Finder" to get the name every disk whose name starts with "M" |
With Scripting Bridge, you perform the same task with the filteredArrayUsingPredicate:
method, which takes an NSPredicate
object. So the line above could be written in Cocoa as:
FinderApplication *finder = [SBApplication applicationWithBundleIdentifier:@"com.apple.finder"]; |
// Specify the criterion for inclusion in the array |
NSPredicate *startsWithM = [NSPredicate predicateWithFormat:@"name BEGINSWITH 'M'"]; |
// Filter the array |
SBElementArray *desiredDisks = [[finder disks] filteredArrayUsingPredicate:startsWithM]; |
// Get the name of every item in the filtered array |
NSArray *nameArray = [desiredDisks arrayByApplyingSelector:@selector(name)]; |
Note: Like everything else in Scripting Bridge, the filteredArrayUsingPredicate:
method is lazy—that is, it doesn't actually get the items in the filtered array until you explicitly ask for some property of them (such as their names). So in the code sample above, no Apple event is sent until the last line.
© 2007 Apple Inc. All Rights Reserved. (Last updated: 2007-10-31)
|