NOTE: This document is up to date with iOS 12 and macOS Mojave, but will likely not receive further updates
The comment at the top of the NSUserDefaults.h header file describes the class quite well, so I'll use that to introduce it:
NSUserDefaults is a hierarchical persistent interprocess (optionally distributed) key-value store, optimized for storing user settings.
The 'App' CFPreferences functions in CoreFoundation act on the same search lists that NSUserDefaults does. NSUserDefaults can be observed using Key-Value Observing for any key stored in it. Using NSKeyValueObservingOptionPrior to observe changes from other processes or devices will behave as though NSKeyValueObservingOptionPrior was not specified.
NSUserDefaults is intentionally extremely straightforward under normal circumstances.
When you want to have a setting that controls part of your code, you simply call the appropriate getter method (-objectForKey: or one of the convenient wrapper methods for specific object types) in the relevant section of your code.
If you find yourself needing to do anything else to read a preference, you should take a step back and reconsider: caching values from NSUserDefaults is usually unnecessary, since it's extremely fast to read from. Calling -synchronize before reading a value is always unnecessary. Responding when the value changes is almost always unnecessary, since the nature of "settings" is that they control what a program does when it does it, rather than actually causing it to do something. Having an alternate code path for "no value set" is also generally unnecessary, as you can provide a default value instead (see Providing Default Values below).
Similarly, when the user changes a setting, you simply call -setObject:forKey: (or one of its convenient type-specific wrappers).
If you find yourself to do anything else to set a preference, again, you probably don't need to. It is never necessary to call -synchronize after setting a preference, and users are generally not capable of changing settings fast enough for any sort of "batching" to be useful for performance. The actual write to disk is asynchronous and coalesced automatically by NSUserDefaults.
It may be tempting to write code that looks something like this:
- (void) applicationDidFinishLaunching:(NSApplication *)app { NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; if (![defaults objectForKey:@"Something"]) { [defaults setObject:initialValue forKey:@"Something"]; } }but this has a subtle long-term flaw: if you ever want to change what the initial value is, you have no way to distinguish between a value set by the user (which they would like to keep) or the initial value that you set (which you'd like to change). It's also somewhat slow to do it this way. The solution is to use -registerDefaults:
… [[NSUserDefaults standardUserDefaults] registerDefaults:@{ @"Something" : initialValue }]; …
This has a multitude of advantages:
You can call -registerDefaults: as many times as you like, and it will combine the dictionaries that you pass it, which means you can keep registration of settings near the code that cares about them.
One tricky thing that's nonetheless pretty common is needing to share some settings between several running processes, whether an extension and a host app, or (on macOS) two or more applications
Back in the (good|bad) old days before app sandboxing, this was straightforward: use [[NSUserDefaults alloc] initWithSuiteName:] with the same name in both processes, and they'd share preferences. Terminology note: "domain" and "suite name" are used interchangeably, and are just an arbitrary string identifying a store of preferences.
In the sandboxed world of modern macOS and all iOS versions, NSUserDefaults is initially limited to operating in your app's sandbox; if you use -initWithSuiteName: you'll just get a new store of user defaults that's still not shared. To share it, you need to do two things: get a shared sandbox container to put it in, and use the identifier of that container as the suite identifier you pass into NSUserDefaults when you create it. I'm not going to go into detail on sandboxing here, but you can find the relevant documentation here. Once you have your apps or extensions added to a group, the suite name matching that group identifier will automatically be shared.
I generally recommend sharing as few defaults as possible, just because programs are easier to understand and maintain when values aren't changing out from under them.
NSUserDefaults does not have any form of transaction system, so there's no way to guarantee that multiple changes will only be seen all at once. Another program could see the first change before the second finishes.
Ubiquitous (i.e. stored in iCloud) defaults are currently only supported in the shared educational iPad mode, so are out of scope for this broad discussion. For the time being, use NSUbiquitousKeyValueStore for outside of educational mode. A few tricky bits of ubiquitous defaults are mentioned in the pitfalls section.
Despite the focus on simplicity, there's still a number of ways to get in trouble.
NSUserDefaults has evolved significantly over the years. The list here is accurate as of iOS 12 and macOS Mojave, but is longer in older systems and will likely be shorter in future ones.
If one process sets a shared default, then notifies another process to read it, then you may be in one of the very few remaining situations that it's useful to call the -synchronize method in: -synchronize acts as a "barrier", in that it provides a guarantee that once it has returned, any other process that reads that default will see the new value rather than the old value. For applications running on iOS 9.3 and later / macOS Sierra and later, -synchronize is not needed (or recommended) even in this situation, since Key-Value Observation of defaults works between processes now, so the reading process can just watch directly for the value to change. As a result of that, applications running on those operating systems should generally never call synchronize.
A grab-bag of less commonly used features of NSUserDefaults. May contain bees.
In general, NSUserDefaults has good enough performance that it's not worth worrying about. However, there are a few things to be aware of if it becomes an issue (please use a profiling tool like Instruments to check!)
The first time you read a default, it will load all the defaults for that suite into memory. This can take a meaningful amount of time on slower systems. One implication of this is to not store huge amounts of data in defaults, since it'll all be loaded at once. Another is to not have tons and tons of defaults suites, since each one will require its own initial load.
Even if there are no defaults in a domain, some work is incurred discovering that. For example if you have a "enable debug logging" preference, it's usually faster and smaller to have it in your standard user defaults, rather than a separate logging suite.
Once loaded, reading a default is extremely fast; on the order of half a microsecond on a 2012 MacBook Pro. Certain things can invalidate the cache and require reloading it though: if the suite is shared with another process, then setting a default in either process will invalidate the cache in both. In the more typical un-shared case, reading a default after setting one will incur a small amount of overhead, but not a full cache rebuild. The implications here are to avoid unnecessary sharing, and to minimize unnecessary sets, but read freely.
Setting the same key repeatedly, even to different values, can be significantly faster than setting many different keys. This allows cases like saving the size of a window that's live-resizing to be fast.
Setting a value inside a collection inside defaults will set the entire collection. The only "partial write" support is at the top level keys.
Setting a value will (eventually, it's asynchronous and occurs some time later in another process) write out the entire plist to disk, no matter how small the change was. Avoid storing large amounts of data, especially if there are frequent changes.