Posted on October 6, 2009 at 6:13pm EDT. More.

Avoid defining your iPhone app’s default values in two places

This is a guide to using XSLT to extract default values from your iPhone app’s Settings bundle into a separate property list file. That file can be loaded by your app at runtime, sparing you the need to maintain the same data in two places and avoiding the risk of a mismatch leading to buggy behavior.

Your iPhone app includes a Settings bundle, so that you can keep your application simple and leave the settings in the settings app. Specifically, you allow the user to customize the FooColor setting, with a default value of Red.

Initially, the user defaults database doesn’t contain a value for FooColor. Although you defined a default value in the Settings bundle, only the Settings app pays any attention to it.

Take Note: Cocoa uses the term “user defaults” to refer to application settings in general. Don’t confuse that with the notion of a default value, which is the initial value of a setting before it has been changed through user action.

The proper way to load default values at runtime is to use NSUserDefaults’s registerDefaults: method. It takes a single NSDictionary object and merges it into the user defaults registration domain. (The user defaults database is divided into domains, or layers.) Values in the registration domain are only used if they are not defined anywhere else, so you don’t need to worry about overwriting a user’s preference.

You create a property list file named Defaults.plist, define an entry giving “FooColor” the value “Red”, and load it:

NSString *path = [[NSBundle mainBundle] pathForResource:@"Defaults" ofType:@"plist"];
NSDictionary *dict = [NSDictionary dictionaryWithContentsOfFile:path];
[[NSUserDefaults standardUserDefaults] registerDefaults:dict];

The only problem with this set up is that you must keep your Settings bundle and Defaults.plist files in sync. If you change the default FooColor from Red to Blue, you must change both. If you add a new setting, you must add it to both. If you’ve been programming for more than a month, you know that eventually you will make a mistake which will lead to a bug that will take forever to track down and take years off your life.

Therefore, a better solution is to dynamically generate Defaults.plist from the data that is already in the Settings bundle. You can do so with ExtractDefaults.xsl, a simple stylesheet which creates an XML property list from a Settings bundle definition, and xsltproc, which is fortunately already installed on your Mac.

First, if you really did create a Defaults.plist, remove it from your project and delete the file. It’s graduating from a source file to a build product!

Then, select your target in Xcode. Go to the menu bar and select Project | New Build Phase | New Run Script Build Phase. A window appears. In the Script area, enter the following shell script:

SETTINGS_PLIST=$SRCROOT/Settings/Root.plist
XSL=$SRCROOT/ExtractDefaults.xsl
DEFAULTS_PLIST=${BUILT_PRODUCTS_DIR}/${WRAPPER_NAME}/Defaults.plist
xsltproc -o $DEFAULTS_PLIST $XSL $SETTINGS_PLIST
plutil -convert binary1 $DEFAULTS_PLIST

You may need to edit the first three lines, depending on the path to your Settings page file and where you saved ExtractDefaults.xsl. It’s also recommended that you fill in the Input Files and Output Files lists, so that Xcode can intelligently determine when the script needs to be run (when input files are newer than output files). Following the script above, Input Files should contain:

$(SRCROOT)/Settings/Root.plist
$(SRCROOT)/ExtractDefaults.xsl

Output Files should contain:

$(BUILT_PRODUCTS_DIR)/$(WRAPPER_NAME)/Defaults.plist

Note that Xcode needs round parenthesis around variable names, unlike the shell script, which uses nothing or curly braces. There’s a good reason for this, but I don’t know what it is.

That’s it! Now, when you build your app, the script will generate a Defaults.plist file inside of your app bundle, so it can be loaded by your code. And you won’t have to worry about your app defaults mismatching your Settings bundle defaults, because it is being handled by a machine, and machines never fail!

Mini-FAQ

Where do I get ExtractDefaults.xsl? Click here to download the file. It’s free to use for whatever you want to do with it. If you improve on it in some way, kindly let me know.

What if I have also default values not defined in my Settings bundle? You can manually enter them into another property list file, and load both. It’s totally cool to call registerDefaults: several times with different dictionaries. Totally cool!

What if my Settings bundle contains multiple page files? In that case you could run the script over each one, generating several Defaults.plist files, and load them all at runtime. Or, if you’re really clever, figure out some way to merge them all into a single property list at build time. I can’t do everything for you.