Vincent Gable’s Blog

December 19, 2008

Undocumented Automator.framework Goodness: Actions Library

In Leopard, Apple introduced AMWorkflowView and AMWorkflowController, which let you easily add automator-editing capabilities to your application (modulo bugs!)

Obviously, for editing a workflow to be useful, you need to be able to add actions to it. And to do that, you need to be able to browse and search actions. But the current Automator.framework has no official support for doing this.

But there are undocumented private APIs, which Automator.app (link launches it) uses…

How to Find Them

class-dump is an amazingly useful command-line tool that lets you generate headers from a compiled Objective-C binary. Download it, and point it at /System/Library/Frameworks/Automator.framework/Automator, to see everything Automator.framework really lets you do.

Instant Library Panel

The most useful definition I found was,
@interface NSApplication (AMLibraryPanel)
- (void)orderFrontAutomatorLibraryPanel:(id)sender;
@end

Calling [NSApp orderFrontAutomatorLibraryPanel:nil]; will show a panel with the same action-library view Automator has.

This is what I’m using right now in IMLocation to let people find actions for workflows. I don’t like the idea of using private APIs. But I like the idea of implementing my own AMLibraryView replacement even less. It would be a lot of work to make it as good as the real thing. Any differences give users a fractured experience — one way to do something in Automator, another in my program. And I would get dismal ROI, because I expect Apple to expose this functionality in the (near) future.

(I also wouldn’t be so bullish on using private APIs if I didn’t have workarounds in place right now. The only way orderFrontAutomatorLibraryPanel: gets called is if a user presses a “Show Actions” button on the toolbar. That same toolbar has an “Open in Automator” button that opens the worklfow in Automator, where they can edit it without bugs. So even if orderFrontAutomatorLibraryPanel: stopped working tomorrow, users could still do everything they could before — albeit less elegantly).

Looking Just Like Automator

I don’t recommend doing this, but I’ve been able to embed an AMLibraryView in an NSSplitView next to an AMWorkflowView, to get a more Automator.app-like appearance.

Here’s how I did it, given the IBOutlets workflowViewAndLibrarySplitView which is an NSSplit view with an AMWorkflowView in one side, and the other side’s view connected to the outlet workflowLibraryView,

[workflowViewAndLibrarySplitView replaceSubview:workflowLibraryView with:[[AMLibraryPanel sharedLibraryPanel] _libraryView]];

Will put the library view inside your split view.

Of course this screws up orderFrontAutomatorLibraryPanel:. And I would not expect it to work in two windows. And it’s using an underscore-private method of an already private and undocumented API. That’s just to too risky for me; so I stick with the panel. It gets the job done with one line of code, and that’s good enough for me.

December 12, 2008

One assert() You Need?

Accidently disconnected outlets in shipping Cocoa apps are legend.

Jonathan “Wolf” Rentzsch

From what I can tell assert() is slowly going the way of the goto in the programming world. Exceptions, unit-tests, and other modern software engineering practices, seem to have a better answer for testing something at runtime, and ensuring that you never enter a bad state.

But there is one case where I think you should put an assert() (and not an NSAssert()) in your code: in awakeFromNib assert() that every IBOutlet is connected.

It’s surprisingly easy to accidentally disconnect something in Interface Builder, or rename something in Xcode. I’ve done it before. More then once. assert()-ing IBOutlets has saved me a lot of debugging time.

Hard to Find In Code

An IBOutlet that is not connected is nil. Because Objective-C quietly ignores messages sent to nil, it’s very easy not to notice the problem for a while; then spend a long time debugging a side effect of the issue.

Hard to Find By Eye

A widget in a nib/xib file that is not connected will never change it’s state. But it typically it has a reasonable initial state. This makes it difficult to detect disconnected IBOutlets by eye, because things will look right until they are supposed have changed, but the eye is drawn to change, and de-emphasizes unchanging things.

Why assert()?

As Wolf says, the worst-case-scenario is that right before release you make a trivial change in Interface Builder (“We can’t ship with that window saying “Claculator”), an IBOutlet gets disconnected, and nobody notices in time.

But an assert() failing will be noticed with casual testing, and triggered if the nib is loaded at all, even if the series of interactions needed to cause the widget to change state are not performed.

An assert() is very light-weight and easy to do. It’s exactly one line of code, and you don’t have to add a unit testing framework to your project to do it. And that makes it perfect for test projects. I’m a believer in getting a feature working in a test project first, then copying it into your real product. (Honestly I don’t do it as much as I should, and I almost always kick myself for it.) It lets you test and learn without hacking-up your product. Plus, new things are often easier to try without the weight of a big code base.

Speaking of unit tests, I don’t have complete faith in them here. That’s because a disconnected IBOutlet in a shipping program is fundamentally an issue with the way the release-build is configured. So anything that’s not testing the the actual release build is not exhaustively testing for this problem. an OCUnit-test can catch a disconnected IBOutlet if it is loaded into the application. But in my experience, such tests are cumbersome. Running them involves launching your full application, and programatically manipulating it. This can take a while to run since all the UI will be displayed and animated. And you have to be careful about state since each test will be changing the state of your program.

Ultimately, assert() just seems to be optimal, in dependability and simplicity, for catching a disconnected IBOutlet.

But I suspect there’s a better way I don’t know.

How do you catch IBOutlets?

Powered by WordPress