Scripting Bridge Release Notes for Mac OS X v10.5

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.

Contents:

The Benefits of Scripting Bridge
How Scripting Bridge Works
Lazy Evaluation
Automatic Application Launch
Filtering Arrays


The Benefits of Scripting Bridge

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:

How Scripting Bridge Works

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.

Generating "Glue" Code for Scripting Bridge

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:


AppleScript 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.

Lazy Evaluation

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.

The get Method

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.

Taking Full Advantage of Laziness

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

Automatic Application Launch

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.

Filtering Arrays

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)


Did this document help you?
Yes: Tell us what works for you.
It’s good, but: Report typos, inaccuracies, and so forth.
It wasn’t helpful: Tell us what would have helped.