NSUserDefaults—some pretty good practices

“How do I store user preferences in my iOS app?” is a pretty common question most developers face at some point in their app development career. Perhaps and app can display information in different, user-selectable units, perhaps the user should be able to enable or disable certain features or perhaps the developer wants to keep track of certain aspects of the state of the app so that it can be restored at a later time. In all of these and many other cases, storing this information in the user defaults system and accessing it with NSUserDefaults is a good solution. In this blog post, I hope to explain the user defaults system and what NSUserDefaults is, how it works and some pretty good practices to follow when implementing these defaults.

What is the User Defaults System?

Before I can address NSUserDefaults and how to make use of it, I want to explain the user defaults system in OS X and iOS. Applications in OS X and iOS use the user defaults system to store preferences. The user defaults system maintains a defaults database that the OS uses to keep track of the various preferences (or defaults) that the user has set at a global or application-specific level. When a developer wants to be able to save preference settings or certain aspects of the state of the application, he or she will be using the user defaults system to interact with that user’s defaults database.

The user defaults system keeps the values stored in its database categorized according to domains. There are five different domains in the user defaults system: the argument domain, the application domain, the global domain, the language domain and the registration domain. For the most part in iOS, developers work with the application and registration domains. The argument domain contains default properties that have been set at the command-line. The global domain contains properties that the OS sets and uses throughout all application and the languages domain records language-specific preference values. In most iOS apps, a developer will seldom need to delve into the default settings in any of these domains.

The application and registration domain however, are where an iOS developer will go to set any application-specific default values and will be the focus of the rest of this post. Every iOS application has its own application domain in the user’s defaults database. This is where any application-specific preferences are stored.

User preferences are stored as key-value pairs. The key is a unique string that identifies a specific preference within a specific domain. Developers are encouraged to keep the values associated with those keys to one of several basic types: strings, numbers, Boolean values or dates. While more complex objects can be stored as generic data, this practice is discouraged since it reduces flexibility.

How to Use NSUserDefaults

Now that we have a basic understanding of the defaults system, let’s move to looking at how to interact with that system. Developers don’t typically interact with the defaults system directly; instead developers use the NSUserDefaults class to interact with that system. NSUserDefaults is included in the Foundation framework and it provides a variety of class and instance method for creating, getting and setting preferences for an application.

The most straightforward way of accessing the defaults system is by using a shared instance of the NSUserDefaults object.

[NSUserDefaults standardUserDefaults]

This returns an instance of the shared user defaults object initialized with all the defaults currently set in the user’s defaults database. With this shared object, it is possible to set and get any of the defaults in the user’s defaults database.

Setting Default Values

NSUSerDefaults uses the same method for setting values of keys as when setting keys in an NSMutableDictionary: setObject:forKey.

For example, if you want to save a display name, a score and a Boolean value indicating whether a level was completed, you could write:

[[NSUserDefaults standardUserDefaults] setObject:@"GreatPlayer" forKey:@"displayName"];
[[NSUserDefaults standardUserDefaults] setObject:@31415962 forKey:@"level1Score"];
[[NSUserDefaults standardUserDefaults] setObject:@YES forKey:@"level1completed"];

This code will set the value of the key displayName to the string “GreatPlayer”, level1score to 3,141,962 and level1completed to YES. If any of these keys do not already exist in the user’s defaults database, a new key will be created and its value set. If the key already exists in the application domain, then the old value will be overwritten with the new value.

NSUserDefaults provides several data-type specific methods for setting default values that can be used rather than the generic setObject:forKey: method. A partial list of such methods is as follows:

  • setBool:forKey:
  • setFloat:forKey:
  • setInteger:forKey:
  • setDouble:forKey:

Converting the above example using these methods would produce:

[[NSUserDefaults standardUserDefaults] setObject:@"GreatPlayer" forKey:@"displayName"];
[[NSUserDefaults standardUserDefaults] setInteger:31415962 forKey:@"level1Score"];
[[NSUserDefaults standardUserDefaults] setBool:YES forKey:@"level1completed"];

Notice we no longer need the @-literals for setting the NSInteger and Boolean values, we can simply pass the appropriate constant as parameters. Since each of these methods calls setObject:forKey: behind the scenes they simply serve to make your code more readable and to provide a thin layer of type checking when setting values.

Getting Default Values

Retrieving values for keys stored in the user’s defaults database is just as straightforward. Like the setters, the most generic getter is the objectForKey: method of NSUserDefaults.

NSString* name = [[NSUserDefaults standardUserDefaults] objectForKey:@"displayName"];
NSInteger score = [[[NSUserDefaults standardUserDefaults] objectForKey:@"level1Score"] integerValue];
BOOL completed = [[[NSUserDefaults standardUserDefaults] objectForKey:@"level1compelted"] boolValue];

An inconvenience with using this method is that a generic object is returned (an object of type id) hence that object needs to be converted to the appropriate type after it is returned. For strings, one can assign an object of type id to an NSString, but for the numeric and Boolean types, you need to extract their value from the object with the appropriate methods (e.g. integerValue, boolValue, etc.) as in the example above.

Again, the NSUserDefaults class provides convenience methods for getting specific data types so that this extra conversion can be avoided.

NSString* name = [[NSUserDefaults standardUserDefaults] stringForKey:@"displayName"];
NSInteger score = [[NSUserDefaults standardUserDefaults] integerForKey:@"level1Score"];
BOOL completed = [[NSUserDefaults standardUserDefaults] boolForKey:@"level1compelted"];

Using Constants

In all the above examples, string literals have been used for key values. As any good coder knows, use of such “magic numbers” is discouraged because it makes the code more brittle and error prone. In fact, look closely at the above examples and you will notice an error. When setting the level completed key, the key used was “level1completed”, however when retrieving the value of that key, the key was typed as “level1compelted” (the e and the l are transposed). This is a prime example of what can happen when having to type out such keys by hand. When trying to fetch the value of the level completed key, the key as typed does not exist and hence NSUserDefaults will return a nil value. This value may be valid in some circumstances, but may be invalid in others perhaps leading to spending time trying to determine why the defaults are not being set properly. As all good programmers know, constants should always be defined and used in all such circumstances.

There are two main ways of defining constants in Objective-C: the #define pre-processor directive and the const keyword. Continuing the example from above, using the #define pre-processor directive to declare constants for the keys is as follows:

#define kUserNameKey @"userNameKey"
#define kLevel1ScoreKey @"level1Key"
#define kLevel1CompletedKey @"level1CompletedKey"

A typical place for such declarations is in a file named constants.h included in the project and #import-ed into where needed. It is a common practice to append “key” to variable names that are used as preference keys. It is also common to prepend “k” to constant values. I combine these two practices when naming my key constants.

An alternative is to use the const keyword to declare a string constants. To do so, create both a header file and an implementation file. In the header (.h) file declare the string constants:

@interface Constants : NSObject

extern NSString* const kUserNameKey;
extern NSString* const kLevel1ScoreKey;
extern NSString* const kLevel1CompletedKey;

@end

And then in the implementation (.m) file, set the values of those constants.

@implementation Utility

NSString* const kUserNameKey = @"userNameKey";
NSString* const kLevel1ScoreKey = @"level1Key";
NSString* const kLevel1CompleteKey = @"level1CompletedKey";

@end

And then when getting or setting these defaults use these constants instead.

[[NSUserDefaults standardUserDefaults] setObject:@"GreatPlayer" forKey:kUserNameKey];
[[NSUserDefaults standardUserDefaults] setInteger:31415962 forKey:kLevel1ScoreKey];
[[NSUserDefaults standardUserDefaults] setBool:YES forKey:kLevel1CompletedKey];

and

NSString* name = [[NSUserDefaults standardUserDefaults] stringForKey:kUserNameKey];
NSInteger score = [[NSUserDefaults standardUserDefaults] integerForKey:kLevel1ScoreKey];
BOOL completed = [[NSUserDefaults standardUserDefaults] boolForKey:kLevel1CompletedKey];

I believe that using either method (#define or const) is equally effective. Some people argue that one method is more efficient than the other however I have not yet found any such arguments to be persuasive. I recommend using whichever method you are more comfortable with. Either method is absolutely superior to not using constants at all. Using these constants allows one to make use of the autocomplete feature in Xcode. When needed, start typing the constant and Xcode will suggest the completion. This makes entering code faster and less prone to errors. Using defined constants centralizes their declaration in one place. In the future when a key’s name needs to change (notice I say, “when” not “if”) there is only one place to go to change that key’s name. This eases code maintenance and shortens the time to make changes in the future. Finally, by using declared constants and autocompletion, a coder is more likely to use long, descriptive names for constants which results in more readable code. If a programmer has to type out the full key in numerous places, he or she is more likely to choose shorter, easier to type key names that are less understandable to others.

Missing Values

In the example above about the misspelled key, I mentioned that since the key was not found in the application domain, the value returned by boolForKey: (or objectForKey:) was nil which resulted in a value of NO (or 0 or false) to be set. This is normal behavior for all the getter methods in NSUserDefaults&emdash;they return nil when accessing keys that do not exist. As a result of this behavior, many developers will have code such as this in their programs:

NSString* name = [[NSUserDefaults standardUserDefaults] stringForKey:kUserNameKey];
if (!name) {
    name = "somedefaultvalue";
}

Defensive programming at its finest. While getting a value of nil back from these methods may be acceptable in some, or even most cases, it is still poor programming practice. For the same reasons we should always initialize variables when declared, we should always initialize defaults so they have sensible values. This way, those values can be safely retrieved anywhere in code confident that valid values will be returned.

Many coders write something like the above in the app delegate file in the application:didFinishLaunchingWithOptions: method. They will check to see if all the default preference values have been set, and if not, set them. This isn’t necessarily a bad approach, but there is a better approach fully supported by NSUserDefaults.

NSUserDefaults has a method called registerDefaults: that takes an NSDictionary of key-value pairs as a parameter. It will then use those key-value pairs to create an in-memory representation of default values for all those preferences and store them in the registration domain of the defaults system. Continuing the above example, we could code the following in the app delegate file in the application:didFinishLaunchingWithOptions: method.

NSDictionary* defaults = @{kUserNameKey:@"GreatUser", kLevel1ScoreKey:@0, kLevel1CompletedKey:@NO};
[[NSUserDefaults standardUserDefaults] registerDefaults:defaults];

This ensures that all the default values for the application are set and available in memory. Later in the app, there is no need to perform a conditional check to ensure that a default value has been set. Furthermore, if valid default values are 0, if a 0 is returned when retrieving a default, you are confident that is a valid value. Also, by removing the conditionals, code is shortened and easier to understand and maintain. Less code is oftentimes better.

To synchronize or not to synchronize

Another practice frequently seen is to call the synchronize method on NSUserDefaults every time a new default value is set.

[[NSUserDefaults standardUserDefaults] synchronize];

I don’t know that this is necessary and it may even be harmful to the performance of an application.

Recall that the user defaults system maintains a database, written to disk, that stores all the default preference values for an application. When the application needs that information, it is read from disk and a copy of that information is kept in a cache in memory so that repeated requests for that information don’t incur a performance penalty. iOS will periodically synchronize the contents of the in-memory cache of the defaults with the database written to disk. Calling synchronize manually forces that exchange to happen. Doing this frequently could result in a performance penalty and adds what could be needless lines of code. For the most part, developers can rely on the automatic synchronization provided by iOS. The only place where I feel is may be important to manually call synchronize is when the application is being sent to the background. A user may be using the app and a phone call may come in or they may hit the home button or otherwise wish to switch to another application. In these circumstances, the app may be in a state where a change has been made to one of its default values but that value may not have yet been saved to disk. Calling synchronize in the app delegate’s applicationWillResignActive: method will ensure that any changes made to the default values have been saved. When the app is restored, all the user’s most recent defaults will be safely available to the application.

NSUserDefaults Pretty Good Practices

Given what I know about NSUserDefaults and the defaults system in iOS, I’ve developed a short list of what I call, “pretty good practices” to implement when creating user defaults. I call them pretty good practices because I don’t know that they necessarily qualify yet as industry best practices. Best practices arise from a consensus of a community as to the best way of doing things. What follows are my pretty good practices that may someday evolve into industry best practices.

  1. Define and use constants for keys. In general, this is an industry best practice anyway but I’m extending it specifically to the use of keys for defaults. By defining and using constants for keys you get the benefit of Xcode’s autocomplete feature and avoid problems with mistyping lengthy key names.
  2. Use registerDefaults: in the app delegate file. Create an NSDictionary of all the default values needed in the application and use registerDefaults: to register them in the application:didFinishLaunchingWithOptions: method. This way you can be confident that all defaults have been set to valid values and can be accessed anywhere in the application.
  3. Avoid overuse of synchronize. There is no need to call synchronize every time a default value has been set. Synchronize is periodically invoked automatically so it rarely needs to be called manually. Do call it manually in the applicationWillResignActive: method to ensure that any default values that have been changed have been written to disk.

I hope this has been useful. If you have any questions or comments, feel free to contact me.

jdabrowski

jdabrowski

Prior to joining Accella, Jim spent several years teaching computer science at the college level. He earned his Ph.D. in Computer Science from the University of Wisconsin-Milwaukee, where his studies focused on human-computer interaction. He has taught in many areas of computer science including introductory courses, programming in various languages, and web-design courses.

10 Responses

  1. Thanks for the article, i used to code in a c# and am struggling with developing in objective c while sorting out the documentation. What you wrote and the way you presented it was extremely helpful.

  2. Thanks a lot for the good article, very well explained. I hope to see more articles like this in the future.

  3. Great article! Answered all of my questions about best practices for NSUserDefaults.

  4. can we use archiver and unarchiver to save the value in disk without using NSUserDefault? is that also write bec that also write on disk and retrive it later?

Leave a Reply

Your email address will not be published. Required fields are marked *

Categories

Search

Recent Posts

Most Common Tags