Working without a nib, Part 5: No, 3!

For all you desperate souls waiting in line at the Moscone Center, and you more desperate souls waiting in line at MacRumors, take heart, because there’s something even more desperate than you — NSApplicationMain(). It’s so desperate to load a nib that it’ll take the first one it can find. When your application launches, NSApplicationMain() instantiates the NSPrincipalClass from your app’s Info.plist and calls +[NSBundle loadNibNamed:owner:] with the instance as the owner. This method in turn calls +[NSBundle bundleForClass:] with your NSPrincipalClass and -[NSBundle pathForResource:ofType:] with type @"nib". If your Info.plist contains no NSMainNibFile key, then the nib name and path arguments for those methods are nil. Why in the world would your Info.plist be missing NSMainNibFile? See Part 1 of this series. If that doesn’t answer the question, see Part 2. If that doesn’t answer the question, see Part 3.

When I set the NSPrincipalClass key to my custom NSApplication subclass, the corresponding bundle for that class is my app’s main bundle, so if there’s no nib in the bundle, the app fails to launch with the error, No NSMainNibFile specified in info dictionary, exiting. However, when I leave NSPrincipalClass as NSApplication, the corresponding bundle turns out to be /System/Library/Frameworks/AppKit.framework. If you send the message -[NSBundle pathForResource:nil ofType:@"nib"] to that bundle, it returns /System/Library/Frameworks/AppKit.framework/Resources/English.lproj/NSAlertPanel.nib, which is the first nib file in the English.lproj folder. As a consequence, NSApplicationMain() attempts to load NSAlertPanel.nib and set the file’s owner to your app’s NSApplication instance. That particular nib file contains several buttons with the action buttonPressed: targeted at the file’s owner, but unlike NSAlert, which is specified as the class of the file’s owner in the nib, NSApplication doesn’t implement buttonPressed:, so you get the error, Could not connect the action buttonPressed: to target of class NSApplication. Mystery solved! And I would have gotten away with it too, if it wasn’t for those meddling kids!

There are a number of ways to handle this problem. My preferred workaround, which I’ve implemented in the revised version of the Nibless project, is to set NSPrincipalClass to JJApplication, call [[JJBundle class] poseAsClass:[NSBundle class]] in main.m, and override an NSBundle method in JJBundle.m:


+(BOOL) loadNibNamed:(NSString *)aNibNamed owner:(id)owner {
    if (!aNibNamed && owner == NSApp) {
        // We're lying here. Don't load anything.
        return YES;
    } else {
        return [super loadNibNamed:aNibNamed owner:owner];
    }
}

We now return to our regularly scheduled WWDC speculation. (I predict that everyone in the audience will get a car.) If you are attending The Keynote on Monday, remember to bring plenty of Scooby snacks. If you’re playing the home game: every time Steve says “cool”, drink!

2 Responses to “Working without a nib, Part 5: No, 3!”

  1. Jack Nutting says:

    It seems like you’re jumping through lots of hoops that could be avoided by just calling the setAppleMenu: private method that Joar suggested in part 1. You said you’d rather not use that in production code, but does your current strategy of relying on private ivars and undocumented behavior really seem *less* fragile?

    That being said, I’m enjoying reading about your investigations into the nether regions of Cocoa. Be careful out there! :)

  2. Jeff says:

    I’ve reconsidered my opinion and written a new post on this issue. I don’t think that I’m jumping through any more hoops by using setValue:forKey: instead of setAppleMenu:, though. The two techniques are functionally equivalent; the latter technique doesn’t automatically solve the other problems I’ve discussed.