Home > cocoa > Scripting Bridge

Scripting Bridge

April 27th, 2009

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

  1. Add ScriptingBridge.Framework to your project:
    • Open Targets
    • Double-click the Target
    • Select the General Tab
    • Click “+” for Linked Libraries
    • Add ScriptingBridge.framework
  2. 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.
  3. 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.
  4. Include the new header file in your program
    • In your .m file:
      #import <ScriptingBridge/SBApplication.h>
      #import "Mail.h"
  5. 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.

  • Share/Bookmark
Categories: cocoa Tags: , ,
  1. mark e. barron
    June 13th, 2009 at 13:02 | #1

    I need to find someone out there who is interested in the same and who has the understanding to redirect me toward a solution. Am I trying to do the impossible? I believe AppKit.h is not normal for iPhone. I get errors relating to it as you will see below.

    Following instructions, 1. I added ScriptingBridge.framework to the General Tab of the Target Info Page

    1. I selected the Rules Tab of the Target Info Page and added the custom script sdef “$INPUT_FILE_PATH” | sdp -fh -o “$DERIVED_FILES_DIR” –basename “$INPUT_FILE_BASE” –bundleid defaults read "$INPUT_FILE_PATH/Contents/Info" CFBundleIdentifier

    followed by : the info for ‘with output files’

    1. I created a Group(folder) in my Project ,name AssociatedApp and dragged Mail.app there after unselecting the “Copy Items into destination group’s folder”

      Next we are instructed to :

      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.

      1. Include the new header file in your program.

    end of instructions;

    1. When I drag Mail.app to the Rules Tab of the Target Info Page I see only one place that I get a cut and paste ‘green Plus sign paste accepted” icon. This is in the box with the text beginning with “sdef “$Input_FILE”, inserting a blank first, I get

      sdef “$INPUT_FILE_PATH” | sdp -fh -o “$DERIVED_FILES_DIR” –basename “$INPUT_FILE_BASE” –bundleid defaults read "$INPUT_FILE_PATH/Contents/Info" CFBundleIdentifier /Users/appleuser/Cocoa/iHungry7/Mail.app

    I have an source file from which I will attempt to email with an attachment. at the top of this *.m file I paste, as directed: #import #import “Mail.h”

    I find the newly created Mail.h file as predicted in DerivedSources below the build folder. I copy it to my project and include it in the project, using ‘add existing file’.

    I then Build as directed and get multiple errors. I am stuck . Many Many thanks for reading. Mark

    cd /Users/appleuser/Cocoa/iHungry7 setenv PATH “/Developer/Platforms/iPhoneSimulator.platform/Developer/usr/bin:/Developer/usr/bin:/usr/bin:/bin:/usr/sbin:/sbin” /Developer/Platforms/iPhoneSimulator.platform/Developer/usr/bin/gcc-4.0 -x objective-c -arch i386 -fmessage-length=0 -pipe -Wno-trigraphs -fpascal-strings -fasm-blocks -O0 -Wreturn-type -Wunused-variable -D__IPHONE_OS_VERSION_MIN_REQUIRED=20000 -isysroot /Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator2.2.1.sdk -fvisibility=hidden -mmacosx-version-min=10.5 -gdwarf-2 -I/Users/appleuser/Cocoa/iHungry7/build/iHungry7.build/Debug-iphonesimulator/iHungry.build/iHungry.hmap -F/Users/appleuser/Cocoa/iHungry7/build/Debug-iphonesimulator -I/Users/appleuser/Cocoa/iHungry7/build/Debug-iphonesimulator/include -I/Users/appleuser/Cocoa/iHungry7/build/iHungry7.build/Debug-iphonesimulator/iHungry.build/DerivedSources -include /var/folders/lJ/lJDuzOmaEX4Q1OyHZP8pTE+++TI/-Caches-/com.apple.Xcode.501/SharedPrecompiledHeaders/iHungry_Prefix-besecngakovcoubtihetjjsibyjz/iHungry_Prefix.pch -c /Users/appleuser/Cocoa/iHungry7/RecipeNoteViewController.m -o /Users/appleuser/Cocoa/iHungry7/build/iHungry7.build/Debug-iphonesimulator/iHungry.build/Objects-normal/i386/RecipeNoteViewController.o /Users/appleuser/Cocoa/iHungry7/RecipeNoteViewController.m:16:42: error: ScriptingBridge/SBApplication.h: No such file or directory In file included from /Users/appleuser/Cocoa/iHungry7/RecipeNoteViewController.m:17: /Users/appleuser/Cocoa/iHungry7/Mail.h:5:26: error: AppKit/AppKit.h: No such file or directory /Users/appleuser/Cocoa/iHungry7/Mail.h:6:44: error: ScriptingBridge/ScriptingBridge.h: No such file or directory In file included from /Users/appleuser/Cocoa/iHungry7/RecipeNoteViewController.m:17: /Users/appleuser/Cocoa/iHungry7/Mail.h:134: error: cannot find interface declaration for ‘SBObject’, superclass of ‘MailItem’ /Users/appleuser/Cocoa/iHungry7/Mail.h:140: error: syntax error before ‘SBObject’ /Users/appleuser/Cocoa/iHungry7/Mail.h:141: fatal error: method definition not in @implementation context compilation terminated. {standard input}:32:FATAL:.abort detected. Assembly stopping.

  2. mark e. barron
    June 13th, 2009 at 13:04 | #2

    typo above: I actually correctly added two imports

    import

    import “Mail.h”

  3. mark e. barron
    June 13th, 2009 at 13:05 | #3

    I actually included the files named below ScriptingBridge/SBApplication.h main.h

    The pointy brackets were throwing the script.

  4. June 13th, 2009 at 16:33 | #4

    Scripting Bridge does not exist on iPhone. The Mail.app you are importing here is a completely different application from the iPhone application, so wouldn’t be helpful even if Scripting Bridge were available. You may be able to eventually get it compiling on Simulator because Simulator uses the Mac’s version of Foundation, but it can’t work on iPhone because iPhone doesn’t provide an Applescript interface to other applications.

    The only interface 3rd party apps have to the Mail app on iPhone in 2.2 is the mailto: URL, which is limited, but all we really have.

  5. June 24th, 2009 at 09:23 | #5

    Thanks for this Rob. I tried to get Scripting Bridge working a few months ago and eventually gave up. I mention this because I was trying to convert an Applescript that ran perfectly, but I couldn’t get certain properties with Scripting Bridge.

    It’s a long, long time ago and I can’t remember the specifics except that I was working with System Events, but it’s worth noting that in some edge cases, Scripting Bridge won’t allow you to access some areas that AppleScript does.

    So I eventually resigned myself to using a third party tool called AppScript – http://appscript.sourceforge.net/

    Hope this is helpful to someone else for who Scripting Bridge isn’t suitable.

  6. Dalton Hamilton
    September 24th, 2009 at 11:22 | #6

    Hi. Excellent article. I’ve written a ScriptingBridge Cocoa app in Xcode and it works fine for text emails and attachments. However, I want the attachment to show in the body of the email on Outlook email clients. To do this, I need to convert the email to HTML by adding a header of “Content-Type: text/html”. Do you know how to do this or know where I can find other classForScriptingClass objects — specifically for headers? Best Regard Dalton Hamilton Chapel Hill, NC

  7. September 24th, 2009 at 12:10 | #7

    I’ve seen no easy way to create an HTML mail through Applescript. You can write the HTML to a file and read it in, but that’s a lot of work. And I’ve seen folks who pass keyboard events, but that’s very fragile.

    There is a private property called “htmlContent,” but I’ve never been able to read or write it. There’s also a “source” property, but it’s readonly. The mail headers are not modifiable on outgoing messages.

    I don’t know if anyone has solved this problem in a good way.

  1. No trackbacks yet.