NOTE: This document is up to date with iOS 12 and macOS Mojave, but will likely not receive further updates

What is NSUserDefaults?

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.

Hierarchical:

NSUserDefaults has a list of places to look for data called the "search list". A search list is referred to by an arbitrary string called the "suite identifier" or "domain identifier". When queried, NSUserDefaults checks each entry of its search list until it finds one that contains the key in question, or has searched the whole list. The list is (note: "current host + current user" preferences are unimplemented on iOS, watchOS, and tvOS, and "any user" preferences are not generally useful for applications on those operating systems):

Persistent:

Preferences stored in NSUserDefaults persist across reboots and relaunches of apps unless otherwise specified.

Interprocess:

Preferences may be accessible to and modified from multiple processes simultaneously (for example between an application and an extension).

Optionally distributed (Currently only supported in Shared iPad for Students mode):

Data stored in user defaults can be made "ubiqitous", i.e. synchronized between devices via the cloud. Ubiquitous user defaults are automatically propagated to all devices logged into the same iCloud account. When reading defaults (via -*ForKey: methods on NSUserDefaults), ubiquitous defaults are searched before local defaults. All operations on ubiquitous defaults are asynchronous, so registered defaults may be returned in place of ubiquitous defaults if downloading from iCloud hasn't finished. Ubiquitous defaults are specified in the Defaults Configuration File for an application.

Key-Value Store:

NSUserDefaults stores Property List objects (NSString, NSData, NSNumber, NSDate, NSArray, & NSDictionary) identified by NSString keys, similar to an NSMutableDictionary.

Optimized for storing user settings:

NSUserDefaults is intended for relatively small amounts of data, queried very frequently, and modified occasionally. Using it in other ways may be slow or use more memory than solutions more suited to those uses.

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 Basics: The 99%

NSUserDefaults is intentionally extremely straightforward under normal circumstances.

Reading settings from NSUserDefaults:

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).

Storing user settings in NSUserDefaults

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.

Providing Default Values

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.

Sharing Defaults Between Programs

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.

Sharing Defaults Between Devices

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.

Caveats and Pitfalls: StackOverflow Redirects To This 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.

Advanced NSUserDefaults: You Probably Don't Need This

A grab-bag of less commonly used features of NSUserDefaults. May contain bees.

NSUserDefaults Performance Tradeoffs: Gotta Go Fast

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.

Other writing about programming

An eldritch horror of a tic-tac-toe program twisting NSUserDefaults to its own ends