Matt Gallagher tells how he reverse engineered the link between Xcode and Interface Builder. Very interesting, I learned a lot. I’ve done essentially the same thing with iChat. (And in retrospect it might have been a bad idea, because it’s broken on Snow Leopard).
February 5, 2009
December 26, 2008
Always Update the View From the Main Thread
I wish I’d read this years ago:
AppKit, the GUI framework, is not thread safe. In order for things to work properly, you (almost) always need to update GUI classes from the main thread
—Dave Dribin (slightly edited)
I’ve run into UI + threading problem before, but I’d just never seen this limitation of AppKit spelled out.
Dave’s article explains how to call code on the main thread better then I can.
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 IBOutlet
s 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.
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 IBOutlet
s 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 IBOutlet
s 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 an OCUnit-test can catch 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.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 IBOutlet
s?
October 29, 2008
Two Help Menus
I ran into an issue at work where sometimes an application would have two “Help” menus, on OS X 10.5 “Leopard” (but not on OS X 10.4 “Tiger”). The problem was interacting with the UI before the application had finished enough of the AppKit-initialization process.
The application had to install a component, which involved displaying authentication dialogs and such. Because the component was necessary for the application to work correctly, I thought it would be safest to do this as early as possible. But displaying a dialog in code called from awakeFromNib
ended up being the cause of the double Help Menu issue.
Waiting until applicationWillFinishLaunching:
or applicationDidFinishLaunching:
to interact with the user fixed the problem. (In my case, it was safe to defer the installation until then.)
I am not aware of any other issues from putting up a window “too early” … however, it seems to me that doing it is asking for trouble. The AppKit/Cocoa environment obviously isn’t 100% ready at that point. Why risk running your code with half-baked libraries if you don’t have to?
Whenever possible, I will defer “first run” behavior until applicationWillFinishLaunching:
or applicationDidFinishLaunching:
.
August 20, 2008
Localizing In Xcode
Let me say this again in slow motion: NEVER type in ANY English string without typing
NSLocalizedString()
around it! This will save you SO MUCH HASSLE later on when your app is popular. Remember that enterprising polyglots can localize your code from just the binary you ship if you follow a few rules of localization, so you may wake up one day and find that someone from across the world has mailed you a your app in another language. It’s a fuzzy feeling and it gets you instant market-share.
May 25, 2008
Objects that Won’t Hide
NOTE: Although this specific Bug Bite is about NSTextView
, and the “hidden” property, the same underlying issue applies to other interface-objects (NSTableView
, etc.), and different properties, like size.
Problem
If you send a setHidden:YES
message to an NSTextView
, and it’s text disappears, but the view itself (white box) stays visible here’s the problem, and the solution.
It turns out that if you created the NSTextView
by dragging it off the pallet in Interface Builder, then it’s not an NSTextView
. It’s an NSTextView
wrapped inside an NSClipView
inside an NSScrollView
. The NSScrollView
is what puts up the scroll-bars if the NSTextView
gets really big; the NSClipView
helps make the scrolling work.
So if text
is your IBOutlet
to your NSTextView
, then when you say [text setHidden:YES];
, the NSTextView
is hidden, but the the total package won’t disappear, unless you hide the NSScrollView
as well.
Solutions
You can send the message to NSScrollView
containing text
, like so:
[[text enclosingScrollView] setHidden:YES];
.
This will hide everything inside the NSScrollView
, including text
.
Another solution is to create just an NSTextView
in Interface Builder. To do this, put an NSView
in your interface (it’s called a “Custom View”,in the Interface Builder objects pallet). Then select it, bring up the object inspector (cmd-shift-i), choose the “custom class” from the category menu at the top, and select NSTextView
from the list of subclasses. This puts an NSTextView
in your interface, without the surrounding clip and scroll views. Unfortunately, it also means you can’t configure it in Interface Builder, beyond resizing it. That’s why I’m not partial to this approach, although I have used it.
Thanks to ZachR for suggesting enclosingScrollView
.
May 14, 2008
NSAlert + Sheets + Threads = Inexplicable Bugs
UPDATED 2008-12-26: in general, all AppKit code should be called on the main thread.
Problem:
When using an NSAlert
to display a sheet in a multi-threaded application, unexpected badness can happen.
I was using
beginSheetModalForWindow:modalDelegate:didEndSelector:contextInfo:
To display an NSAlert
as a sheet.
But when the sheet appeared, the window it was attached to disappeared and got into some weird broken state where it would appear iff the application was not frontmost.
Fortunately, I remembered having encountered weirdness with NSAlert
sheets before. The symptoms were different (previously the alert didn’t have focus), but the same solution still worked.
Solution: make sure the message to display the sheet is sent by the main thread. To do this, put the call to beginSheetModalForWindow:modalDelegate:didEndSelector:contextInfo:
inside another method, showMyAlert
, then use performSelectorOnMainThread:withObject:waitUntilDone:
to make sure showMyAlert
is called on the main thread.
Work around use runModal
to display the alert as a modal dialog instead of a sheet. runModal
Does not appear to have any problems when called from other threads.
Just like last time:
The whole incident feels funny to me. I suspect there may be some deeper issue at work that I am not aware of. When I have time to investigate further I shall update this post. Unfortunately I don’t have time to look into ‘solved’ bugs today.
UPDATED 2008-12-26: in general, all AppKit code should be called on the main thread.