Wednesday, September 24, 2008

Toying around ... #2

A few pointless posts ago, I was talking about Nibless Cocoa applications, and how Jeff Johnson, had a ready to use solution for us, Interface Builder cheaters. Jeff's elegant solution is build around using Cocoa NSObject's ability to have a class pose as another class (but it must be a parent class). The trick been to make sure that when the application is starting, the NSBundle created to load the main NIB file does not load it, and instead just pretend to have done so. The method works like a charm (granted that the entry NSMainNibFile in info.plist has been removed), however there is just a little problem: the class method poseAsClass has been deprecated in OS X 10.5 and is not even present in 64-bit Cocoa. I have no idea whatsoever this is been phased-out by Apple. Since a similar facility exists in Objective-C and GNUStep, it's not clear at all why this have to go ... Security concern maybe? Whatever the explanation, I'd like to know ...

So ... since I'm interested in a solution that can work from 10.4 and beyond on PowerPC and Intel, 32-bit as well as 64-bit, I had to go back to the drawing board and come-up with an alternative solution, likely something not as elegant. Hopefully, thanks to Objective-C's dynamic abilities, it is possible to replace on-the-fly a method implementation by another function (as long as it have a compatible signature). Scott Stevenson have made a an excellent post on this, which I used as the base of my solution.

The idea is relatively simple: replace the NSBundle's class method loadNibNamed:owner: by my own implementation function and call the original method only in when necessary. Since I wanted to have this automatically done as part of a special type of application, I derived NSApplication in HZNiblessApplication, implementing its object method init to do the dirty work:


- (id) init {
if ((self = [super init]))
{
Method lMethod = class_getClassMethod([NSBundle class],@selector(loadNibNamed:owner:));
if(!lMethod)
NSLog(@"NSBundle+loadNibNamed:owner: not found!");
else
{
#if MAC_OS_X_VERSION_MIN_REQUIRED > MAC_OS_X_VERSION_10_4
gDefaultNSBundleLoadNibNamed = method_setImplementation(lMethod,(IMP)loadNibNamed);
#else
gDefaultNSBundleLoadNibNamed = lMethod->method_imp;
lMethod->method_imp = (IMP)loadNibNamed;
#endif
}
}
return self;
}
view rawThis Gist brought to you by GitHub.

As your can see, the method basically retrieve the method within the NSBundle class, and swap its implementation with a function called loadNibNamed. Since we need to call the original method to perform its duty when there is a need to load a bundle, we save the pointer to the function in a global variable named gDefaultNSBundleLoadNibNamed. Let's have a look now on how that replacement function is implemented:


BOOL loadNibNamed(id aObject,SEL aSelector,NSString *aNibNamed,id aOwner)
{
if (!aNibNamed && aOwner == NSApp)
return true;
else
return (BOOL)(*gDefaultNSBundleLoadNibNamed)(aObject,aSelector,aNibNamed,aOwner);
}
view rawThis Gist brought to you by GitHub.

From there, all that is needed is to replace the value of the NSPrincipalClass key in the application's info.plist by HZNiblessApplication and get your application delegate to fill in the Application menu from -applicationWillFinishLaunching: ... Okay, yeah I'm simplifying a bit here, but you get the point I'm sure :-P

As you have noticed, I finally figured a way of putting neatly formatted code (hmm ... at least on Firefox) on this blog, courtesy of github.com.

No comments: