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

Two tips for iTunes 9, which was driving me absolutely crazy before I found the workarounds:

  • Option+clicking the little arrows next to albums no longer drills down to the album, but instead just shows the artist tracks, because the default column view (which is now vertical) only has “Artist” selected. Go to View -> Column Browser and select “Album” to get the old functionality back. I also recommend selecting “On Top”, unless you have loads of horizontal screen real-estate.
  • The green “Zoom” button on the main iTunes window now behaves just like any other OS X window – which is to say, it simply does something weird with the window size. Option+clicking will hide the window and display the mini player. Interestingly, the “Zoom” button still works the same way in the mini player as it did in iTunes 8.

Posted on Thursday, September 10th, 23:35. Filed under: Mac OS X

I’m lazy – really lazy. So when I decided to create a free version of Converted, my first thought was: “How do I set this up so building the two versions and keeping them in sync requires no extra effort?”.

Since I’m no Xcode expert, it required some deliberation (and digging), but eventually I came up with a solution that fulfilled these requirements:

  • Both versions should be built from the exact same codebase; no branching or separate projects.
  • After the initial setup, building either version should be no more than a couple clicks; no edits whatsoever!

The solution hinges upon simply using 2 different Info.plist files. Apart from some Apple-specified keys that have to be set in each file, I also added (amongst others) a key for a boolean that controls the version (eg. XXIsLite). Within the code, the different functionality between the 2 versions is controlled by observing that boolean value.

Now normally, this solution is A Very Bad Idea™: a sneaky user of the trial version could just open up the bundle, change the key and gain access to the full version. On the iPhone however the bundles are signed, so this isn’t possible (I’m obviously discounting jailbroken iPhones that bypass the codesigning – anyone who goes to that much trouble to save a couple bucks is beyond help).

So here’s the process I followed, more or less:

  • In the ‘Groups & Files’ list, select your info dictionary file; by default, this is called ‘Info.plist’. Add a new line, set the key to something along the lines of ‘XXIsLite’ and the value to ‘NO’.
  • Again in the ‘Groups & Files’ list, expand the ‘Targets’ group. Right-click on the target that builds the app (in my case, Converted). Select ‘Duplicate’. Rename it if you like, eg. ‘Converted Lite’.
  • At this point it’s best to change the product name as well, so right-click on the new target and select ‘Get Info’. In the ‘Build’ tab, select ‘All Configurations’ from the configurations pull-down. Type ‘product name’ in the search box; change the ‘Product Name’ setting to something else, eg. Converted Lite.
  • Back in the ‘Groups & Files’ list, notice that Xcode has also duplicated your ‘Info.plist’ file and created ‘Info copy.plist’. Rename the new file to something else, if you like, such as ‘Lite-Info.plist’.
  • Setup the new info dictionary. At a minimum, you must change the bundle identifier to be different from the full version (com.codingcurious.ConvertedLite in my case) and set the value for the ‘XXIsLite’ key to ‘YES’.
  • Optionally, you can define a different home screen icon, a different display name or any other key-values you’d like to be different on the trial version.
  • Lastly, if you haven’t already, implement the code that will differentiate the trial version. Define certain “choke points” (the fewer the better) in your code where you check which version it is and modify or subtract certain features. For example, in the flip view Converted adds an additional “solicitation view” for the trial version that directs to the full version download page.

When you’re done, all you have to do to build either version is simply set the active target and build. This way, you can release updates for both the full and trial version with no extra effort at all!

Tip: when directing to the App Store, replace ‘itunes’ in the link with ‘phobos’; this will send the user directly to the App Store, without first launching Mobile Safari. For example,

http://itunes.apple.com/WebObjects/MZStore.woa/...

will first launch Mobile Safari and then redirect to the App Store, while

http://phobos.apple.com/WebObjects/MZStore.woa/...

will open the App Store immediately.

Posted on Thursday, January 29th, 20:12. Filed under: Converted, iPhone, Xcode

Next Page »