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.