6
votes

My apps use [NSUserDefaults standardUserDefaults] as a quick and dirty database to store state about the user and the app itself. The trouble with NSUserDefaults is that its flexibility allows for a big mess down the line, for example when different files all set and read different keys in the dictionary in their own way. You don't get to enforce rules, you can screw up the key name etc..

I wrote a simple singleton "manager-style" wrapper for NSUserDefaults which both takes care of setting the default values when used, hides the name of keys used to fetch the values and encapsulates some extra logic, such as encoding to NSData, when storing and retrieving objects from the store.

At this point they're properties backed by a read/set accessor, but something is rubbing me wrong about it and I'm wondering if perhaps there's a more elegant way of achieving the same result. There's quite a bit of boilerplate and the syntax ends up being somewhat unpleasant. To give you an example:

.h:

@interface UserDefaultsManager: NSObject

+ (UserDefaultsManager *)sharedInstance;

@property (nonatomic, assign) NSInteger somethingImTracking;

@end

and .m:

NSString * const kSomethingImTracking= @"SomethingImTracking";

@implementation UserDefaultsManager

[...]

- (NSInteger)somethingImTracking
{
    return [[[NSUserDefaults standardUserDefaults] objectForKey:kSomethingImTracking] intValue];
}

- (void)setSomethingImTracking:(NSInteger)somethingImTracking
{
    [[NSUserDefaults standardUserDefaults] setInteger:somethingImTracking forKey:kSomethingImTracking];
}

and to access:

NSInteger foo = [UserDefaultsManager sharedInstance].somethingImTracking;
1
I'm doing exactly the same thing in one of my apps to encapsulate a set of preference settings my app supports. BTW - you should make kSomethingImTracking static since it is only used in this .m file. - rmaddy
@rmaddy Great tip, thanks! - Alexandr Kurilin
You might also want your setSomethingImTracking such that (a) it might be prudent to do synchronize after setting the value; and (b) maybe do willChangeValueForKey before setting the value and didChangeValueForKey after setting it, in case you ever implement KVO in the future. But I agree that this is a good way to isolate the particulars of your NSUserDefaults settings. - Rob
Unnecessary synchronization (and on 10.8+ that's "virtually all synchronization") is immensely slower, though still not all that bad. Don't do it, it's voodoo, and you should understand how NSUserDefaults works instead. If you see a case in a 10.8+ single-process Mac app where synchronize is useful, that's worth filing a bug about. It shouldn't be (multi-process, non-NSApplication, and iOS are still slightly more complicated, but even there the rules for when synchronize is needed aren't hard) - Catfish_Man
Also, a background queue won't help. You'll still be holding CFPreferences internal locks and blocking reads from the app's domain while the synch is in progress. - Catfish_Man

1 Answers

4
votes

Personally I use string constants to store the key names, and just access the user defaults object directly, but I don't tend to use defaults to a huge extent, or in many different classes.

One improvement I'd make to your code is to have them all as class methods instead. There is no benefit to a singleton (you're not maintaining any state, that's all in the defaults object) and this takes a bit of the ugly repetitive code (...sharedInstance) out of your use pattern.

synchronize is not necessary to include every time you set. It is only needed when accessing defaults values from different threads in quick succession. The OS calls it itself periodically as well.