Home > User Experience > Keeping User Defaults Synchronized with Settings.bundle

Keeping User Defaults Synchronized with Settings.bundle

The first time you wade into the NSUserDefaults system in Cocoa, you feel sweet relief wash over you. Managing user preferences in any application can be a very tiresome chore. NSUserDefaults is like hiring a housekeeper, only it doesn’t cost you anything.

Excited with the possibilities, you dive in. You quickly have your Settings.bundle added to your project and your settings menus built. It all looks impressive, and only took a few hours. Woo hoo! You launch your app to watch this sorcery in action, when—bang! The app crashes. Not cool. A little debugging shows that the defaults you created on the root page work fine, but only if you open the your app’s Settings page before launching the program. And child menus have the same problem, except that users have to navigate to each child menu before the defaults work. You imagine the reaction you’ll get when you explain this to your users: I have to do whaaaat?

No worries, though. This gap in expectations has been well discussed on teh intertubes. In addition, Apple does provide a reference implementation in the AppPrefs project. However, the AppPrefs project only really points you in the right direction. The other two implementations, which extend the AppPrefs example, are a little too complex for my taste. In addition, none of these three take advantage of one of NSUserDefaults cooler features: defaults domains. When we code along in our app, we don’t want to worry about whether or not a user default has already been “initialized.” We want to put things in our Settings.bundle plists and have them Just Work.

To do that, we need to do a few things. We should really register our defaults before we do anything else. That means it should go in our UIApplicationDelegate’s applicationDidFinishLaunching: method, right at the top.

Second, we need to submit a dictionary of all default values to the standardUserDefaults singleton. It will put them in the Registration domain, which is separate from where your user’s overrides are kept. The registration domain only exists in memory, so we must recreate it every time we run the app. When you request a key value from NSUserDefaults, it will first look for a user-supplied value. If it can’t find one (in memory or on disk), it will look to the values in the Registration Domain, and return that one. That means we need to register all of the values included in our Settings.bundle property lists, whether we think we’ll need them or not. Make no assumptions. Sooner or later, we will (and if we don’t, then we should pull them out of the .plist). This also requires us to load the .plist file on disk for each child pane (if you have any).

If you think about this system for a bit, though, it does make sense. We are already storing the defaults in our .plists. Why store a duplicate copy in the NSUserDefaults application domain database? By not doing so, we avoid the need for synchronization, conflict resolution, etc. We only store the user-specified values in our app’s user defaults. That’s why our keys return nil, unless either we first register defaults, or unless the user manually sets the values (which opening the relevant Settings pane does). Why not have NSUserDefaults automatically registerDefaults like we do below? Well, I’m not sure. You could always submit a feature request to Apple.

In the meantime, then, here is my solution to add to the mix:

/** Loads user preferences database from Settings.bundle plists. */
+ (void)initSettingsDefaults
{
	NSUserDefaults *standardUserDefaults = [NSUserDefaults standardUserDefaults];

	//Determine the path to our Settings.bundle.
	NSString *bundlePath = [[NSBundle mainBundle] bundlePath];
	NSString *settingsBundlePath = [bundlePath stringByAppendingPathComponent:@"Settings.bundle"];

	// Load paths to all .plist files from our Settings.bundle into an array.
	NSArray *allPlistFiles = [NSBundle pathsForResourcesOfType:@"plist" inDirectory:settingsBundlePath];

	// Put all of the keys and values into one dictionary,
	// which we then register with the defaults.
	NSMutableDictionary *preferencesDictionary = [NSMutableDictionary dictionary];

	// Copy the default values loaded from each plist
	// into the system's sharedUserDefaults database.
	NSString *plistFile;
	for (plistFile in allPlistFiles)
	{

		// Load our plist files to get our preferences.
		NSDictionary *settingsDictionary = [NSDictionary dictionaryWithContentsOfFile:plistFile];
		NSArray *preferencesArray = [settingsDictionary objectForKey:@"PreferenceSpecifiers"];

		// Iterate through the specifiers, and copy the default
		// values into the DB.
		NSDictionary *item;
		for(item in preferencesArray)
		{
			// Obtain the specifier's key value.
			NSString *keyValue = [item objectForKey:@"Key"];

			// Using the key, return the DefaultValue if specified in the plist.
			// Note: We won't know the object type until after loading it.
			id defaultValue = [item objectForKey:@"DefaultValue"];

			// Some of the items, like groups, will not have a Key, let alone
			// a default value.  We want to safely ignore these.
			if (keyValue && defaultValue)
			{
				[preferencesDictionary setObject:defaultValue forKey:keyValue];
			}

		}

	}

	// Ensure the version number is up-to-date, too.
	// This is, incidentally, how you update the value in a Title element.
	NSString *version = [[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleVersion"];
	NSString *shortVersion = [[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleShortVersionString"];
	NSString *versionLabel = [NSString stringWithFormat:@"%@ (%d)", shortVersion, [version intValue]];
	[standardUserDefaults setObject:versionLabel forKey:@"app_version_number"];

	// Now synchronize the user defaults DB in memory
	// with the persistent copy on disk.
	[standardUserDefaults registerDefaults:preferencesDictionary];
	[standardUserDefaults synchronize];
}

It’s important to remember the difference between the Registration Domain, and the Application Domain.

The general process is:

  1. Create a “myDefaults” dictionary.
  2. Load a .plist file into a dictionary.
  3. Copy only your default key/object pairs into “myDefaults.”
  4. Repeat (2) and (3) for each .plist in your Settings.bundle.
  5. “Register” your defaults using registerDefaults: by passing it your “myDefaults” dictionary.
  6. Synchronize.

When you perform step 5, NSUserDefaults will create a separate domain for your key/object pairs in myDefaults. If a search of user-supplied values (a.k.a. the Application Domain) for your key turns up nil, NSUserDefaults then search for that key in the Registration Domain.

Let’s walk through an example.

Assumptions:

  • Your Root.plist includes “app_player_name” and a default value of “Player 1”
  • Your Root.plist includes “app_game_difficulty” and a default value of “Easy”
  • The user has overridden “app_game_difficulty” and set it to “Hard”

Scenario 1:

myPlayerName = [[ NSUserDefaultsstandardUserDefaults] objectForKey:@"app_player_name"]

In this case, myPlayerName will equal “Player 1.” NSUserDefaults searches the application domain for @”app_player_name”, and gets nil. Next, it searches for @"app_player_name" in the registration domain and returns @"Player 1" since we had loaded it there using initSettingsDefaults.

 myGameDifficulty = [[ NSUserDefaultsstandardUserDefaults] objectForKey:@"app_game_difficulty"]

In this case, the values of both variables will be:

myGameDifficulty == @"Hard"
myPlayerName == @"Player 1"

NSUserDefaults searches the application domain for @"app_game_difficulty", and returns @"Hard". Now, if it had searched the registration domain, it would have found @"Easy". However, since the user-supplied value takes priority over the registered default, it still returns @"Hard".

Scenario 2:

Now, if had not registered any defaults in Scenario 1, this is what we would have ended up with instead:

myGameDifficulty == @"Hard"
myPlayerName == nil

Why? Because the user-supplied value of @”Hard” was stored in the Application Domain. It didn’t matter that the Registration Domain was empty. Since there was neither a user-supplied value in the Application Domain or a registered value in the Registration Domain, myPlayerName returned nil.

The key here is to remember that setting a default value, and registering a default value are two very different things.

Happy, er, Defaulting!

Advertisements
  1. 08/30/2009 at 11:56 AM

    Thanks for the link – especially since it’s w/ ‘intertubes’ – awesome!

    • Zack
      08/30/2009 at 12:40 PM

      Glad you appreciate the technical jargon.

      And thanks for posting your article. I know sometimes one can end up wondering if spiders are your only readers, so it’s nice to receive some feedback (or trackbacks, as the case may be).

      And congrats on your 1st years as an indy dev!

  1. 03/15/2011 at 2:37 AM

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: