While System Preferences.app generally makes it very easy for users to install and update prefPanes, it has some idiosyncrasies. Foremost is the fact that when it comes to comparing two prefPanes, apparently only the names are compared: make a copy of an already installed prefPane, rename it and double-click; you’ll be offered to install it, rather than replace the existing one.

The second weirdness is a bit more complex, and can present problems when replacing a prefPane with one that is significantly different: on the one hand there is the installed prefPane, and on the other the new one, with the same name but a different bundle identifier. As mentioned above, System Preferences.app considers these two to be the same, based on their shared name.

When installing the new prefPane, System Preferences.app presumably calls unload on the old bundle and then load on the new one. This causes the new bundle’s executable to be launched, but for some reason the bundle is still set to the old one. So for example, initWithBundle is called with the old bundle. Also, any resources fail to load, since they presumably didn’t exist in the old bundle.

The best-case scenario in this case is that the prefPane will load, but no images will be displayed. Image loading is non-fatal, so System Preferences.app will complain in the log about not finding the image resources, but otherwise continue normally. However, if there are any vital resources that need to be loaded, such as Core Data models, the prefPane will probably crash.

This scenario only affects the first launch, when the new prefPane replaces the old one. Afterwards, everything loads and works properly. But it’s hardly the best first impression to give to a user who’s upgrading.

So how to fix it? Since there’s no way to modify the way the prefPane is loaded, one viable solution is to skip that first load, quit System Preferences.app and launch it again. For the user, the process is seamless: after accepting the prefPane update, System Preferences.app will briefly disappear and reappear in the dock and the prePane will load. Apart from the dock icon, the rest of the process is unaffected in the eyes of the user.

Quitting and relaunching the prefPane has to be done with a small tool, which is located in the Resources folder of the new bundle. This is the code that does the magic, somewhere in initWithBundle (preferably the first item of business):

if ([[bundle bundleIdentifier] isEqual:OLD_BUNDLE_IDENTIFIER]) {	
		
	NSString *reloadPath = [[bundle bundlePath]
		stringByAppendingPathComponent:@"Contents/Resources/reload"];
	NSTask* task = [[NSTask alloc] init]; 
	[task setLaunchPath:reloadPath]; 
	[task setArguments:[NSArray arrayWithObject:
		[[NSBundle bundleForClass:[self class]] bundlePath]]]; 
	[task setStandardInput:[NSPipe pipe]]; 
	[task launch];
	[NSApp terminate:nil];
	[task release];
}

The path of the reload tool has to be manually set, rather than via NSBundle’s convenience methods, since they will give the wrong Resource folder. The reload tool takes one argument, which is the path of the new prefPane to launch. So essentially the prefPane sets up a task to launch itself, and then terminates.

The tool is very simple, all it does is launch the prefPane:

int main(int argc, char **argv) {
	
	char dummy; 
	read(STDIN_FILENO, &dummy, 1); 
	CFURLRef url = CFURLCreateFromFileSystemRepresentation(
		kCFAllocatorDefault, (UInt8*)argv[1], strlen(argv[1]), FALSE); 
	CFArrayRef url = CFArrayCreate(kCFAllocatorDefault, (const void**)&url,
		 1, NULL); 
	FSRef ref;
	OSStatus status = LSFindApplicationForInfo(0, 
		CFSTR("com.apple.systempreferences"), NULL, &ref, NULL)
	if ( status  == noErr) { 
		LSApplicationParameters parms = {0, kLSLaunchDefaults, &ref,
			 NULL, NULL, NULL, NULL}; 
		LSOpenURLsWithRole(url, kLSRolesAll, NULL, &parms, NULL, 0); 
	}
	CFRelease(url);
}

In Xcode, the target that builds the prefPane should have a dependency on the tool so that it is also built, and also copy the build result into the Resources folder.

Posted on Wednesday, May 12th, 00:16. Filed under: Automatic, Cocoa, Mac OS X, Xcode

There was a feature I wanted to implement in Automatic 2.0 that required registering the prefPane to handle a specific (custom) URL scheme to pass data into the app. After a lot of digging, I came to the inevitable conclusion: it can’t be done. This post describes both my findings regarding URL schemes in prefPanes and the workaround I used.

Normally, an app can register specific URL schemes with LaunchServices, telling the system that if a user clicks on a URL, it should be forwarded to that app. The information about the URL schemes is stored in the app’s Info.plist, under the CFBundleURLTypes key.

Unfortunately, that doesn’t work with prefPanes (as usual: the prefPane’s bundle is not the app’s bundle). What’s very interesting however, is that apparently Apple’s Accounts prefPane can handle URLs with the macosxserverinvite scheme.

So how does it do it? Looking at the Info.plist file for System Preferences.app, it normally registers the macosxserverinvite scheme, but also adds the key preference pane identifier, with a value of com.apple.preferences.users which is the bundle identifier for the Accounts prefPane. It seems that when System Preferences.app intercepts a registered URL, it forwards it to the prefPane whose bundle matches the preference pane identifier key-value. The big problem here is that this set up is not extensible: as is the norm for Apple apps, System Preferences.app is signed, which means no touching the Info.plist file to add additional URL schemes and redirects (and even without the signing, editing another app’s bundle is very naughty).

That’s where the trail ends. There doesn’t appear to be any way to register a URL scheme for a prefPane inside System Preferences.app. However, there is a workaround: use a helper app.

The process goes like this: register a background helper app to handle the URL, and when the helper intercepts a URL, either process the information itself, or store the data and then launch the prefPane. Of course, because the helper can only launch the prefPane, but not provide it with any other information, it is the prefPane’s responsibility to check every time it launches if there is any URL data stored that hasn’t been handled.

It’s quite a hassle, but the end result for the user is seamless: click on the URL, the prefPane launches and takes appropriate action.

Posted on Thursday, April 22nd, 04:05. Filed under: Automatic, Cocoa, Mac OS X, Xcode

After spending the better part of a day trying to figure why the help book wouldn’t open in Help Viewer, I finally discovered the reason. Hopefully anyone else trying to dig through Apple’s surprisingly verbose, yet conflicting and confusing documentation will stumble across this.

In general, there are quite a few hoops to jump through to get a help book up and running. And some additional ones to get it in a preference pane. But even if you follow Apple’s instructions, you’ll just get the annoyingly non-specific error “Help Viewer cannot open this content”.

The problem is, as usual for preference panes, that the bundle is not the app’s bundle, but rather a guest bundle to System Preferences.app. When Apple says that a help book is automatically registered, they mean the main bundle’s help book. For a preference pane, you have to manually register it.

In Snow Leopard, that’s just a couple lines of code using NSHelpManager, which you can drop somewhere like awakeFromNib:

NSBundle *myBundle = [NSBundle bundleForClass:[self class]];
[[NSHelpManager sharedHelpManager] registerBooksInBundle: myBundle];

In earlier versions of Mac OS X, you’ll have to use AHRegisterHelpBook() from Carbon’s Help framework (it’s quite annoying to have to link an otherwise 100% Cocoa app to Carbon just for this). It requires a few more lines of code, but the effect is the same:

FSRef fsref;
NSBundle *myBundle = [NSBundle bundleForClass:[self class]];
NSString *path = [myBundle bundlePath];
NSURL *url = [NSURL fileURLWithPath:path];
if (CFURLGetFSRef((CFURLRef)url, &fsref)) {
	OSStatus err = AHRegisterHelpBook(&fsref);
	if (err) // Do error stuff
}

Posted on Thursday, April 15th, 00:22. Filed under: Cocoa, Mac OS X, Xcode

The thing that struck me most while working on the iPhone was how limited the resources actually are, something that isn’t (and shouldn’t be) obvious to the user. Going from the simulator to the actual device can be really jarring, because animations that seemed butter-smooth suddenly become a slideshow.

It can be very frustrating to go back to the code and try to figure out just what the issue is, so here are a few things to keep in mind while coding, performance-wise:

  • Transparencies are expensive; wherever possible, set views to be opaque, so there won’t be any wasted cycles refreshing the views underneath.
  • Always reuse cells; the docs seem to suggest that it’s simply a “good practice”, but it’s practically required. If you have a small table you might think it won’t matter to just allocate new cells, but once you start to scroll, the performance hit becomes obvious.
  • It’s better to customize cells by subclassing UITableViewCell rather than add subviews to the content view, especially if transparency is involved. I’m not entirely sure why this is the case, just that subclassing eked out a few extra frames during scrolling.
  • With normal-sized scroll views, loading subviews on-demand is actually worse. What is normal-sized? In my case, I tried up to 3200-by-460 and preloading the subviews yielded much smoother animations than any of the several load-on-demand methods I tried.

With most performance concerns, the common underlying issue is object allocation. Preloading definitely helps, but go too far and you start to bump your head against the memory ceiling. In the end, you want to balance object allocation and definitely use didReceiveMemoryWarning to clear out unused objects.

Overall however, I really liked working on the iPhone: it forces you to go back to the basics and come up with creative solutions: elegant coding is always better than the sledgehammer approach!

Posted on Saturday, January 10th, 18:11. Filed under: Cocoa, iPhone