You’re a good developer. You’ve tackled lots of tough problems they’ve thrown your way. You are respected by your peers. You’ve done a great job with your life! And then there’s that nagging in the back of your head. You can’t kill the guilt of that glaring omission – the horror of horrors. The place where your code just doesn’t stack up.
You’ve got sucky AppleScript support.
Don’t beat yourself up. Apple is to blame for this, and they should step up and start taking responsibility. Adding AppleScript support to an application, even with the pleasure/pain of Cocoa scripting, is as much of a rite of passage for developers as learning to write AppleScripts is for users. That is, it’s a nearly universally agreed upon royal pain in the ass. And that’s wrong.
Why is it so painful? With Cocoa Scripting it’s common to hear that adding support is almost as simple as flipping a switch, and it sort of can be. But it’s also true that skydiving is as easy as finding something tall to jump off of. Now how do we go about ensuring a safe landing?
I talk to a lot of developers through mailing lists, IRC, and even live in person. I’ve never met one who was completely confident about adding scripting support to an app. Even the smartest Mac programmers I’ve ever met tend to confess that they “just tweaked things until they magically worked, then stopped touching it.”
This mirrors my own experience. So let me vent some things that I think are wrong with Apple’s handling of “the AppleScript situation.”
- Inadequate Documentation. The Cocoa Scripting Guide is pretty frickin’ good but it falls short on some frustrating levels. The step-by-step is encouraging, but it fails to mention key points like “for some features, an sdef just totally won’t work, and you’ll want to resort to using old-fashioned script suites or even an ‘aete’ resource.”
Worse are the misleading details about indexed array operators. I ran into this with FlexTime. I need the scripter to be able to manipulate the list of activities associated with a given document. My code-level name for these objects is “TimedEvent.” The documentation suggests that methods of this form should do the trick:
- (void)insertObject:(TimedEvent *)newEvent
inTimedEventsAtIndex:(int)index
But in practice, I had to google my way through blood, sweat and tears to discover that this form was required for scripting:
- (void) insertInTimedEvents:(TimedEvent*)newEvent
atIndex:(int)index
Ugh! Kill! Kill!
- Buggy Examples. Like any good programmer, you’re looking to stand on the backs of giants. In the case of scripting, you’re going to want a good “scripting definition” file, consisting of all the standard Cocoa-supplied handlers, and any custom goodies you add on yourself. So that “standard handlers” stuff should be easy to copy and paste, right? If you look for such a starting point, you’ll find several options. And they all seem to be buggy. It’s not easy for me to advise a developer starting with AppleScript whether they should use Skeleton.sdef, NSCoreSuite.sdef, or some other less scientific approach. The documentation is equally vague:
If your application supports any of the commands, classes, or other scriptability information defined in the Standard and Text suites, as most do, you should copy the suite elements for those suites into your sdef from an existing sdef file, such as Sketch.sdef.
It doesn’t matter which path you take, you’ll end up banging your head bloody against the keyboard as you try to decipher endless, frustratingly nuanced bugs. Script results will come back with confusing chevron values instead of class names:
«class » «constant ****all »
of script item "Text Input - 2 Btns"
Confusing console log messages will appear, complaining that methods like “scriptingAnyDescriptor” are not implemented in your objects. You’ll wonder aloud what the hell that method is for, and even ask around on mailing lists and search google, only to find the plaintive cries of other frustrated developers.
There should be no “hunt-and-gather” phase in adding Scripting support to an application. Apple should include a perfectly functioning standard sdef as part of the Xcode templates for all application targets. If they want good scripting support, they need to throw us a bone.
- Spotty e-Mail Support. There’s a mailing list dedicated to allegedly solving the problems of frustrated scripting support implementors. The problem is, nobody from Apple reliably steps up to answer the most vexing questions posed there. Inevitably the tough questions either go unanswered, or are eventually acknowledged by Dustin Voss or Bill Cheeseman. The two most reliable sources for AppleScript implementation support are non-Apple employees who usually have to painfully admit they only figured it out after hours of scientific experimentation. When Dustin or Bill doesn’t chime in, occasionally an Apple representative does, but it’s highly unpredictable, and more often than not silence reigns. This sucks!
OK, I know, it’s not their “job” at Apple to monitor and reply to issues on the free mailing lists. But maybe it should be. The fact is, if there was anybody on that list replying to questions with the accuracy and authority of somebody like Eric Schlegel on Carbon-Dev, then AppleScript support would be a lot better. We’re treading water, here!
Other groups at Apple also provide stellar support through their lists. Core Audio leaps to mind. USB, Quartz Composer, Cocoa, and Networking also have real experts – the developers themselves – providing quick and friendly support to developers on a daily basis. Most of these groups in fact have 2 or more team members actively tackling problems on the lists. And when something is undocumented or broken, they admit it and we move on. I’m sure there are others. At Apple, helping developers is the rule rather than the exception, and I’m grateful for that.
Thank you, Eric! Thank you Jeff Moore, Bill Stewart, Fernando Urbina, Barry Twycross, David Ferguson, Pierre-Olivier Latour, Chris Kane, Douglas Davidson, Ali Ozer, Becky Willrich, and Quinn! I’m sure there are dozens more that don’t spring to mind or whose lists I don’t read. You’re all heroes, and we appreciate it. Truly.
But we need a Cocoa Scripting hero.
Contained By What?
One of the biggest hurdles in adding Cocoa Scripting support to an application seems to be coming to terms with AppleScript’s “containment hierarchy.” It requires that any scripted object be able to fully specify its location in the containment hierarchy. For instance, in MarsEdit this text paragraph I’m typing right now needs to know that it’s the:
last paragraph of current text of post window “We Need a Hero” of application “MarsEdit”
See, that’s how AppleScript refers to things. And Cocoa Scripting exposes objects directly to AppleScript, so that scripters can manipulate them directly. Long story short, is your Cocoa objects need to know how to tell AppleScript where they live. The way this happens is by way of the “objectSpecifier” method, which most of your scriptable objects will end up needing to implement.
This was a big mental block to me, because I’m not used to “back-referencing” from my objects to their containers. Maybe it’s my application design naiveté, but I find it awkward that in FlexTime for instance, my “cue action” object should know that it’s owned by a particular activity in a particular document. This just seems clunky to me. But as far as I know we’re stuck with it.
But just because the object exposes the objectSpecifier method, doesn’t mean it has to be in charge of it. I’m more comfortable in general with the containing object being responsible for claiming and disavowing ownership of a given object. So I came up with a generic solution that works pretty well for me.
RSContainableObject (Free, MIT License) is a generic NSObject subclass that can have its owning object and relevant key set on it. Then, when somebody asks it for its objectSpecifier, it uses that information to provide the required directions. The actual specification is accomplished by asking the containing object to reckon the object relative to itself, so the parent must itself respond to “objectSpecifier,” either because it’s also an RSContainableObject, or because it implements the method itself.
This approach works for me because I discovered that the vast majority of scriptable objects in my applications are in fact direct subclasses of NSObject. Those that are not, like my NSDocument subclass, inherit the objectSpecifier magic directly from Cocoa. So my typical set up is an NSDocument subclass that contains trees of various RSContainableObject subclasses, which all know how to reckon themselves recursively back up the chain in terms of their container.
These custom model objects end up with declarations that look something like this:
@interface TimedEvent : RSContainableObject <NSCopying>
Then when such a model object is set as a property or added as an element of a containing object, the container takes responsibility by setting the pertinent ownership terms on the object:
// Associate the object with us
[newEvent setObjectContainer:self
containedByKeyName:@"timedEvents"
asToManyRelationship:YES];
The above example is for a “to many” relationship, meaning it’s a member of a list of items. But the same event object could be associated as a property relationship with a similar type of pattern. Here I pretend there’s an attribute of this object called “best damn timer” with an associated Cocoa method of “bestDamnTimer”:
// Disassociate from the old object
[mBestDamnTimer setObjectContainer:nil
containedByKeyName:nil
asToManyRelationship:NO];
// Retain & Release
[mBestDamnTimer release];
mBestDamnTimer = [newTimer retain];
// Associate
[newTimer setObjectContainer:self
containedByKeyName:@"bestDamnTimer"
asToManyRelationship:NO];
By factoring the messy “objectSpecifier” code into one place for a wide variety of use scenarios, I avoid having to engage too often in the (for me, at least) bug-prone ritual of writing that method from scratch.
You might observe that a problem with this approach is that objects can’t be owned by more than one object. That’s true, but that’s also a basic limitation of AppleScript. Objects have “one true specifier” even if there are multiple legal specifiers that lead to it. For instance “default timer of document 1” might resolve to the same object as “timer 3 of application ‘ClockThing'”, but the RSContainableObject relationship should only be between the application and the object. A given object at one time can have only one “canonical” object specifier, and that is the relationship that this class affords.
Hope this helps…. but we still need a hero!
Update: A couple readers have pointed out that I might as well link to some good resources for AppleScript debugging and design:
TN 2106 – Scripting Interface Guidelines. This has a good discussion of the containment hierarchy, what it means for your application, and how it relates to the world-view of the scripter. Thanks, Erik Wrenholt.
TN 2124 – Mac OS X Debugging Magic. This talks about general techniques for debugging AppleEvents and also reminds us of the Cocoa Scripting debugging “defaults” setting NSScriptingDebugLogLevel. I always forget about that and maybe I wouldn’t be quite so tightly wound up about AppleScript if I remembered it! Thanks, Jonathan Wight.