iOS: Yay, In-Application Preferences

There are lots of nice resources (both printed and online) regarding the use of the Settings.bundle to store the user’s preferences on the iPhone, but there seems to be a severe lack of resources about not using it.

Settings.bundle is admittedly pretty cool. With a single .plist file you can create a settings management page in your application that covers a number of common requirements. But besides the obvious UI concern of storing your application’s preferences in a location many of your users will never check, the bigger problem is one of limitations; if the Settings.bundle does what you need, great. If it doesn’t, get the hell out—you can’t put any custom code into the Settings app.

Need to make a query to determine the options for a setting? Not happening. Multi-line text entry? Sorry.

With Raconteur I happily used Settings.bundle in version 1.0 and then hit a brick wall when adding password protection because there’s no way to do password confirmation in the Settings app. And surely I’m not going to leave my application’s settings arbitrarily (from a user’s perspective) split between inside and outside the application, so I have to backtrack and move everything in.

My advice for developers starting on new applications is don’t use Settings.bundle. Unless you know all of your requirements upfront and they couldn’t possibly change (and if you do, I’d like to visit your mythical waterfall). On the whole, it’s exceedingly anti-agile to hit a brick wall and have to redo work. Not to mention breaking user expectations for everyone who had already found the external settings.

Now, if you’re not using the Settings.bundle mechanism, how do you store your user’s preferences? I imagine this is exceeding obvious to experienced Cocoa programmers, but to developers starting with the iPhone, the docs aren’t so clear… the NSUserDefaults mechanism works the same whether you are using Settings.bundle or not. Just grab the defaults object, register some default defaults (…), and start using them:

NSUserDefaults* defaults = [NSUserDefaults standardUserDefaults];
[defaults registerDefaults:[NSDictionary dictionaryWithObjectsAndKeys:
                            @"Cheesy Ramen", @"favoriteFood",
                            @"YES", @"donateToHungryPandas",

Note that this registerDefaults call is important whether you use Settings.bundle or not. You might be shocked (I know I was) to find that your application’s settings (and associated defaults) from your Settings.bundle might not be programmatically available until the user runs the Settings app. Which means that you probably set the defaults once in the Settings.bundle and now you get to set them again in code. I know, lame.

Access your settings like so:

if ([[defaults objectForKey:@"favoriteFood"] isEqualToString:@"Cheesy Ramen"]) {
    // Screw cheesy ramen...
    [defaults setObject:@"Pizza" forKey:@"favoriteFood"];

A few more tips:

  • Definitely consider defining some constants for your preference keys so that the compiler can find your typos instead of your users finding them.
  • Since NSUserDefaults acts like an NSMutableDictionary, it only stores objects (not your bools, ints, etc.). Fortunately it provides nice convenience methods like boolForKey: and setBool:forKey: to abstract away conversions to object types (like boolean YES to @"YES").
  • You can use both Settings.bundle and your own interface to access the exact same preference data with no problems (makes sense but I’m just pointing it out). That said, it probably doesn’t make a great deal of sense from a UI perspective.
  • NSUserDefaults is automagically saved off at regular intervals (and, I think, when you exit the application). You don’t have to worry about saving those preferences, or about exactly where they go. That’s why it’s automagic.
  • Once you remove Settings.bundle from the program, it may still appear to be around, showing up in the Settings app. In reality it won’t hang around on your users’ devices (I believe), but to remove it from your test devices, simply make sure to run Build > Clean All Targets in Xcode.