Scripting Bridge

About

The benefit to using Scripting Bridge is having native access to all of Cocoa coupled with the ability to automate applications. Scripting Bridge requires a different approach than in other languages, however, and comes at a cost in some limited functionality. It is very explicit in how you create objects in way that requires a very specific and limiting approach. The following distills all of Apple’s documentation, which can be unwieldy at times, down to the bare minimum, and notes those specific approaches and limitations of the API.

Prepping Projects

Make the application header

In Terminal, run the following

sdef /path/to/application.app | sdp -fh --basename nameOfHeader

This creates a header file to #import in the class so we know what we can do. In the case of Photoshop, this is extremely long and results in every enum and suite being cataloged, over 3,000 lines long. The file is placed in /Macintosh HD/Users/UserName/nameOfHeader.h

The header also includes the following #import:

#import <AppKit/AppKit.h>
#import <ScriptingBridge/ScriptingBridge.h>

Reading the header file

It’s important to carefully examine the objects you are working in code versus the header as it may not be obvious with which classes you are working. For example, an Excel application that is actually used to create objects follows this inheritance:

Excel2008Application : SBApplication : SBObject : NSObject

But this is different from another application class that sits before the actual usable application class in the header file:

Excel2008BaseApplication : Excel2008BaseObject : SBObject : NSObject

The distinction here is that ”only” SBApplication classes can create objects, and there is usually only ”one” class that is derived from SBApplication.

Writing code

  • The application object must be created before anything else:Excel2008Application *excelApp = [SBApplication applicationWithBundleIdentifier:@"com.microsoft.Excel”];. Some things to note:
    • Excel2008Application is the class type of the application as given in the Excel2008.h” file generated by the command line application
    • @"com.microsoft.excel” is the bundle identifier for the application. This is an application development convention, not an Applescript convention.
  • Commands that originally had one required argument and other optional arguments are now all required.
    • For example, Excel’s native Applescript command open workbookonly requires one argument—the path to the file—and has 11 (eleven) options. In Scripting Bridge, the command requires all 12 arguments be populated.
  • Errors can’t be managed easily: Applescript commands don’t have mechanisms to pass error variables as arguments, and the Scripting Bridge framework doesn’t have any hooks for managing errors on even an application level. There are ways to manage errors for the data you are reading and altering, like if a value ends up being nil). But if a command is sent the wrong argument in turn causing the command to fail (like a wrong datatype), there’s nothing in the framework to capture that error and act upon it. The command will silently fail.
  • Porting can be tricky since approaches are different and now classes are handled strictly. To get around tricky problems kit it is sometimes best to sort out the stack and classes leading up to the command in question in native Applescript, and then evaluating for a solution.

Make the Application Object

The recommended way to create the target application by Apple is to useapplicationWithBundleIdentifier:. The class name—iTunesApplicationimageEventsApplication—comes from the base name given in the terminal command. In the header file, look for the line:@interface nameOfTargetAppApplication : SBApplication

iTunesApplication *iTunes = [SBApplication applicationWithBundleIdentifier:@"com.apple.iTunes"];
imageEventsApplication *imageEvents = [SBApplication applicationWithBundleIdentifier:@"com.apple.imageevents"];

We can also go by the path if we don’t know the bundle ID:

NSURL *pagesURL = [NSURL fileURLWithPath:[NSString stringWithFormat:@"%@/Applications/iWork/Pages.app", NSHomeDirectory()]];
PagesApplication *pagesApp = [SBApplication applicationWithURL:pagesURL];

Note that the class name is dependent on the name of the file you determined, so make one header and maintain that header in your SCM being careful to use the same header on each application upgrade.

The get command.

The get command is one of the linchpins to the whole system: it’s the command that allows you to get the content you need in the format you need. The SBObject class is needed to work with the applications, but get is how you go from an SBObject-derived class to a proper Cocoa class (NSStringNSArray, and the like). From the docs…

”This method forces the current object reference (the receiver) to be evaluated, resulting in the return of the referenced object. By default, Scripting Bridge deals with references to objects until you actually request some concrete data from them or until you call the get method.”

In the case of getting a range of values in Excel, the following line pulls the data out of the Excel2008Range object:

 
NSArray *cellValues = [targetRange.value get];

If we look at just targetRange.value directly we just get another SBObjecteven though the value property shows returning idget actually pulls the object being referenced by the SBObject, in this case an NSStringNSArray, orNSNumber (so far).

Evaluating the object returned by get

It seems every application is different in regards to behavior, and the only documentation about what is returned by get is in the sdef file, but that can be wrong. The easy way to evaluate what comes back from get would be to look at theclass of an object and act accordingly:

theText = [targetRange.value get];
if ([theText isKindOfClass:[NSString class]]) {
	// do something with the string
} else if ([theText isKindOfClass:[NSNumber class]]) {
	// do something with the number
}

The problem that I find with those properties that return id is that I usually have to call get twice in order to act upon something. For example, here is the complete code that is used to evaluate an object pulled from a cell and coerce it into a string:

NSString *theText;
theText = [targetRange.value get];
if ([theText isKindOfClass:[NSString class]]) {
	theText = [targetRange.value get];
} else if ([theText isKindOfClass:[NSNumber class]]) {
	theText = [[targetRange.value get] stringValue];
}

Here I’m declaring a variable of the target data type, populating the variable, evaluating the value, and then coercing the value according to the result. The only way around this is to evaluate in a test case and then hard code the command according to the class.

File Paths

One of the benefits of using Scripting Bridge as opposed to Applescript to select files is that we get the whole functionality of the NSOpenPanel class. But, Applescript requires that we use the HFS file path (Macintosh HD:Users:MyName:Path:To:File) as opposed to the NSURL path (/Users/MyName/Path/To/File). A conversion from one to the other is tucked away in URL Services. The following snippet works with one path (as NSString):

- (NSString *) convertPosixPathtoHfsPath:(NSString *)posixPath isDirectory:(BOOL)isDirectory {

    NSString *firstChar = [posixPath substringToIndex:1];
    if ( ![firstChar isEqualToString:@"/"] ) {
        return posixPath;
    }

    CFURLRef myURL = CFURLCreateWithFileSystemPath(NULL, (__bridge CFStringRef)posixPath, kCFURLPOSIXPathStyle, isDirectory);
    NSString *hfsPath = (__bridge NSString *)CFURLCopyFileSystemPath(myURL, kCFURLHFSPathStyle);
    CFRelease(myURL);
    return hfsPath;

}

Other Resources

Most websites related to Scripting Bridge are focused on Ruby and Python, both arguably easier than Objective-C. But since the focus of this site is on Objective-C, the following is what I have found to be useful. Also, all websites are focused on scripting Apple applications like iTunes, iCal, and the like. Which is all well and good because Apple can easily write their apps to be Scripting Bridge saavy. But the rules change in production environments when dealing with Microsoft Excel and Adobe InDesign, and that’s why you won’t find code for iTunes on this site.

  • Red Sweater Blog: Apple’s Script: Not exactly related to Scripting Bridge, but it does propose coming up with a replacement for Applescript as Apple’s default scripting language. This touches upon some of the reasons why I have looked at alternatives in the first place.