Cocoaphony

Scripting Bridge

Say you want to talk to another app through Applescript. With 10.5, you can much more easily get there from Cocoa without complex forays into CoreServices, Carbon and AppleEvents. The docs on how to do it are a little thin at times (as all Applescript docs are), so let’s walk through it. The relevant docs you’ll want to read are these:

Learning Applescript

Scripting Bridge Framework Reference

Scripting Bridge Sample Code

And most importantly (and most hidden):

SBSystemPref’s Magical README

And now for a step-by-step example. We’re going to send some mail with an attachment through Mail.app. I’m going to assume you know enough Applescript to have written this:

tell application "Mail"
    set theMessage to make new outgoing message with properties ¬
        {subject:"Test outgoing", content:"Test body"}
    tell theMessage
        make new to recipient at end of to recipients with properties ¬
            {name:"Rob Napier", address:"sb@robnapier.net"}
        tell content
            make new attachment with properties {filename:"/etc/hosts"} ¬
                at after last paragraph
            set visible of theMessage to true
        end tell
    end tell
    activate
end tell

Let’s convert it to Scripting Bridge.

Setting up your project

  • Add ScriptingBridge.Framework to your project:
    • Open Targets
    • Double-click the Target
    • Select the General Tab
    • Click “+” for Linked Libraries
    • Add ScriptingBridge.framework
  • Add a rule for creating .h files for scriptable Applications (this should be built into XCode, but isn’t). You could also do this by hand one time, and just add the resulting .h to your project.
    • Select the Rules tab
    • Click “+”
    • Process: Source files with name matching: *.app
    • Using: Custom Script: (the following needs to be one long line):
    sdef "$INPUT_FILE_PATH" | sdp -fh -o "$DERIVED_FILES_DIR" --basename "$INPUT_FILE_BASE" --bundleid `defaults read "$INPUT_FILE_PATH/Contents/Info" CFBundleIdentifier
    • Click the “+” under “with output files:” $(DERIVED_FILES_DIR)/$(INPUT_FILE_BASE).h
    • Close the Target window. We’re done with the really crazy part.
  • Add the application as one of your sources.
    • Drag the desired application (Mail.app) into your Groups & Files tree. You can put it in a group if you like
    • Unselect “Copy items into destination group’s folder” (if it is selected)
    • Drag the application into your “Compile Sources” step in your Target (it should be first, so the .h gets created before it’s needed). Yes, we are “compiling” an application into a header.
  • Include the new header file in your program
    • In your .m file:
      #import <ScriptingBridge/SBApplication.h>
      #import "Mail.h"
      
  • Build
    • Now is a good time to build. That will get your .h created, making everything easier later. It’s created in your DerivedSources directory. The easiest way to open it is with Cmd-Shift-D (Open Quickly). Just hit Cmd-Shift-D, and then type “mail.h”. Once you’ve found it, you can drag it into your Groups & Files list if you like. It will be deleted when you Build Clean, so don’t be surprised by that. Aren’t you glad you learned about Open Quickly? It’s my favorite way to move between files.

Writing the code

OK, we now have everything in place to write some code. The process of converting from Applescript to SB is fairly mechanical, but like all Applescript things there are some things you just need to know. We’re going to take this one line at a time.

tell application "Mail"

We need an SBApplication object to tell things to. So we make one:

MailApplication *mail = 
    [SBApplication applicationWithBundleIdentifier:@"com.apple.mail"];

Notice that you can’t call [[MailApplication alloc] init]. This is more a limitation of the sdp tool we used to create the .h than of ScriptingBridge. There is no Mail.m file to actually implement the MailApplication class, so you can’t directly allocate the class. You’ll see more of this limitation later.

set theMessage to make new outgoing message with properties ¬
    {subject:"Test outgoing", content:"Test body"}

We’re creating a new outgoing message. This includes a special step that you can partially guess, and somewhat just have to know. Every scripting object you talk to has to chain back to the SBApplication. You can’t deal with stand-alone SBObjects. In this case, the Applescript has an implicit step that we need to make explicit. The Applescript above is implicitly adding theMessage to outgoing messages. When you get used to it, it’s kind of obvious, and if you look in Mail.app, you’ll see that MailApplication has an -outgoingMessages property. But it can be a little surprising when you’re getting stared. So let’s rewrite the Applescript to be more explicit:

set theMessage to make new outgoing message at end of outgoing messages ¬
    with properties {subject:"Test outgoing", content:"Test body"}

And so here’s the code:

MailOutgoingMessage *mailMessage =
    [[[[mail classForScriptingClass:@"outgoing message"] alloc]
        initWithProperties:[NSDictionary dictionaryWithObjectsAndKeys:
            @"Test outgoing", @"subject",
            @"Test body\n\n", @"content",
            nil]] autorelease];
[[mail outgoingMessages] addObject:mailMessage];

This is a very common pattern, so it’s worth studying. First, note that we can’t directly +alloc the MailOutgoingMessage. We have to ask for it through the SBApplication object. This is more of the limitation discussed above. And we need to pass the Applescript class “outgoing message.” This is obvious from the MailOutgoingMessage name once you see how sdp creates the .h file. The properties we pass SB are identical to the ones we pass Applescript. And once we create it, we add it into the object tree with -addObject:, which adds “at end of” the list just like we need (just like an NSArray). OK, now go back and read this paragraph again and make sure you’ve got it. We’re going to use this several times.

tell theMessage
    make new to recipient at end of to recipients with properties ¬
        {name:"Rob Napier", address:"sb@robnapier.net"}

You should be able to guess the code for this one:

MailToRecipient *recipient = [[[[mail classForScriptingClass:@"to recipient"] alloc] 
    initWithProperties:[NSDictionary dictionaryWithObjectsAndKeys:
                    @"Rob Napier", @"name",
                    @"sb@robnapier.net", @"address",
                    nil]] autorelease];
[[mailMessage toRecipients] addObject:recipient];

And once more for fun:

tell content
    make new attachment with properties {filename:"/etc/hosts"} ¬
        at after last paragraph

==>

MailAttachment *attachment = [[[[mail classForScriptingClass:@"attachment"] alloc]
    initWithProperties:[NSDictionary dictionaryWithObjectsAndKeys:
        @"/etc/hosts", @"filename",
        nil]] autorelease];
[[[mailMessage content] paragraphs] addObject:attachment];

Those really are the complicated ones (and they aren’t bad at all once you see how to read them). After that, everything should be obvious:

        set visible of theMessage to true
    end tell
end tell
activate

Becomes:

[mailMessage setVisible:YES];
[mail activate];

The Finished Code

So let’s look at the full code now, including a @try/@catch, since Applescript can generate exceptions (more about this below):

@try
{
    MailApplication *mail = 
        [SBApplication applicationWithBundleIdentifier:@"com.apple.mail"];
    MailOutgoingMessage *mailMessage =
        [[[[mail classForScriptingClass:@"outgoing message"] alloc]
            initWithProperties:[NSDictionary dictionaryWithObjectsAndKeys:
                    @"Test outgoing", @"subject",
                    @"Test body\n\n", @"content",
                    nil]] autorelease];
    [[mail outgoingMessages] addObject:mailMessage];

    MailToRecipient *recipient =
        [[[[mail classForScriptingClass:@"to recipient"] alloc]
            initWithProperties:[NSDictionary dictionaryWithObjectsAndKeys:
                    @"Rob Napier", @"name",
                    @"sb@robnapier.net", @"address",
                    nil]] autorelease];
    [[mailMessage toRecipients] addObject:recipient];

    MailAttachment *attachment =
        [[[[mail classForScriptingClass:@"attachment"] alloc]
            initWithProperties:[NSDictionary dictionaryWithObjectsAndKeys:
                    @"/etc/hosts", @"filename",
                    nil]] autorelease];
    [[[mailMessage content] paragraphs] addObject:attachment];
        
    [mailMessage setVisible:YES];
    [mail activate];
}
@catch (NSException *e)
{
    NSLog(@"Exception:%@");
}

Error Handling

I like @try/@catch better than SBApplicationDelegate because the delegate can’t easily interrupt the script if there’s an error. If you let it raise an exception and then @catch it, the entire block aborts, which is what I generally want. This also exactly matches the normal AppleScript error handling pattern.

Summary

Apple has created an incredible new framework with Scripting Bridge, making it easier than ever to tie your application into the system and interact with other programs. Unfortunately, they buried much of the documentation, and left much to the imagination of the reader (like most Applescript documentation). Hopefully this article will help improve that situation and make Applescript a bigger part of your programs.