Tuesday, 5 January 2010

iPhone: Application preferences/defaults/settings. With defaults.

How to extend NSUserDefaults with default values.

Time for more iPhone shenanigans. Lots of people have contacted me saying these posts are useful - thanks. As I do more iPhone development I discover things that aren't immediately obvious from the SDK documentation (excellent as it is). I'm documenting some of them in my blog partly to find out if I've missed the obvious, and partly to give back to the community - Google has certainly pointed me to plenty of other people's blogs with useful answers.

Today's installment: application settings. Or preferences, if that's what you want to call them. Or settings, if you prefer.

Finding the preferred term

The iPhone SDK provides a neat API for persisting simple sets of application settings. It's easy enough to find, if you know what it's called when you search for it. Don't search for preferences, like I did, or you'll be sent down the Core Foundation dead-end of Preferences Programming Topics. This is a rich and fairly heavyweight API for persisting state. But for a simple iPhone app it's overkill.

What you do need to look for is the Cocoa NSUserDefaults class. That's the puppy you want. It's simple when you know what to look for. This class provides an easy way to read and write fundamental data types (integers, floats, bools, NSStrings, etc).

What's missing? NSUserDefault's defaults.

Again, perhaps I've missed something. But this class is missing something fairly obvious. It has a simple set of APIs to write values against a given NSString "key" name, e.g. setInteger:forKey: It has a set of APIs to read those values back for a given NSString "key" name, e.g. integerForKey:

However, there is no way to discover whether or not a setting has been saved yet. This is important since the first time your application is run no preferences have been saved, and you'll need to fall back on a set of well-chosen default values. The NSUserDefaults accessors return "default initialised" values if a key has not been set yet; e.g. 0 for integers, 0.0 for floats, or a nil string. You can't always inspect an integer for zero and replace it with a default - sometimes zero is a valid setting value.

So what can you do about this?

Adding defaults to "defaults"

We can add this functionality to NSUserDefaults easily enough. The first trick is to determine whether a value has been stored for a particular key or not. It turns out that, no matter what kind of key has been stored, internally everything can be accessed as an object and the objectForKey: method returns nil if no value has been stored.

So you can see whether an integer, for example, has been stored by calling objectForKey: with its key name. The returned id value is nil if the integer has not yet been stored, and non-nil if it has.

Useful.

Now, ideally we'd have a suite of methods that read values from the NSUserDefaults class and return a specified default value if that setting has not yet been made. This can be done neatly using Objective-C's class category facility. Categories allow you to extend existing classes with extra functionality.

Here is some example code showing how to do this for some of the basic types. Once you've put this snippet into your application you can use the methods as if they were an original part of the NSUserDefaults API.

@interface NSUserDefaults (ReadWithDefaults)
- (NSInteger) integerForKey:(NSString *)key withDefault:(NSInteger)value;
- (BOOL) boolForKey:(NSString *)key withDefault:(BOOL)value;
- (float) floatForKey:(NSString *)key withDefault:(float)value;
- (id) objectForKey:(NSString *)key withDefault:(id)value;
@end

@implementation NSUserDefaults (ReadWithDefaults)

- (NSInteger) integerForKey:(NSString *)key withDefault:(NSInteger)value
{
if ([self objectForKey:key])
return [self integerForKey:key];
else
return value;
}

- (BOOL) boolForKey:(NSString *)key withDefault:(BOOL)value
{
if ([self objectForKey:key])
return [self boolForKey:key];
else
return value;
}

- (float) floatForKey:(NSString *)key withDefault:(float)value
{
if ([self objectForKey:key])
return [self floatForKey:key];
else
return value;
}

- (id) objectForKey:(NSString *)key withDefault:(id)value
{
id obj = [self objectForKey:key];
if (obj)
return obj;
else
return value;
}

@end

2 comments:

XieXie said...

Thanks for the article.

Foxinfosoft said...

your site is very informative
Thank you
iPhone application development