Learning iCloud Data Management (2014)
Part III: Using the Technologies
9. Using Preferences, Settings, and Keychains with iCloud
Both Cocoa and Cocoa Touch provide a user defaults system that you can use to let users configure their app’s behavior. You may have used it on various projects, but in the era of iCloud, user defaults need a bit more thought. You can easily use iCloud to synchronize defaults across devices, and with that possibility, you now have to think about defaults in a multi-device world. For example, a user might be reading or writing a document that is stored in iCloud. In this case it may be a good idea to keep track of the current location in the document so that when the user picks up another device, reading (or writing or viewing or listening) can continue without interruption.
On the other hand, if the user has chosen an arrangement of document windows on a Mac, you may not want to save those as preferences. One reason not to synchronize the arrangement of windows across devices is that on iOS devices, there is only one window visible at a time. Even with multiple Macs, users may not want the arrangement of windows they use on a large-screen monitor to be replicated on a smaller MacBook screen.
This chapter explores user defaults in general as well as how you can work with them in iCloud-enabled apps. It’s worthwhile to note that the general topic is referred to as user defaults. On OS X, these defaults are often managed with System Preferences, and they often are calledpreferences in user-facing materials. On iOS, the Settings app manages what is often the same type of data, and the term settings is often used.
You’ll explore the main components of user defaults, including property lists and the preferences and settings API.
Working with Legacy Preferences and Settings
You can implement user defaults in any way that you want. There is no need to add a pane to System Preferences on OS X and no need to use Settings on iOS. You can write them out in your own format in a file that your app maintains. A surprisingly large number of apps do just this. It’s not recommended, but the practice is prevalent. It is particularly common with legacy systems and systems that were originally written for other platforms where the OS X or iOS version attempts to try to reuse code that might have been written originally for something as long-ago as a VAX.
The heart of the user defaults settings for iOS and OS X is a set of tools and technologies that work efficiently with structured data. There are two basic tools for doing this: property lists and key-value coding (KVC). They often are used together (property lists rely on KVC, for example).
When you start thinking about converting legacy systems that don’t use the Cocoa and Cocoa Touch tools (and standards!) to iCloud synchronization across devices, take the opportunity to seriously consider changing your structure. It may be more work at the beginning, but by the time you add in the cost of maintaining nonstandard code in what is, after all, a fairly complex new technology (iCloud), it may be worth the investment to use System Preferences and Settings.
Using Property Lists
One way or another, property lists are at the heart of preferences and settings in most cases. They are a fast and efficient way of storing limited amounts of structured data in any of the property list classes (NSNumber, NSData, NSDate, NSString, NSArray, and NSDictionary).
Property lists are pervasive in the Cocoa and Cocoa Touch environments. Considering their importance, you may be surprised to learn that there is no such thing as a property list class. The six property list classes just referenced (that is, the six types of objects that can appear in a property list) comprise all of the classes that you need. Most property lists have a root that is a dictionary (sometimes an array). Thus, the class of such a property list is NSDictionary or NSArray rather than a hypothetical (and nonexistent) NSPropertyList class.
Looking at Property Lists
Developers are used to working with property lists: we use them all the time in Xcode. Figure 9.1 shows the Info tab of a project. (Note that the project itself is selected at the top of the project navigator.)
Figure 9.1 Adjusting the Info property list
As you can see, the property list consists of a number of entries. In the center of each line, you see the type of the entry. Many are strings, but there is also a Boolean, a dictionary, and two arrays. Disclosure triangles let you open the dictionary and array items to reveal their contents. You can often type in values for strings. In some cases, Xcode provides a pop-up menu of choices so that you don’t have to type in a value (and so you won’t type in an illegal value). Xcode also provides pop-up menus to name new entries that you may create.
This is familiar to most developers, but now start to delve more deeply into the property list. You’ll soon see how to integrate property lists with iCloud.
Figure 9.2 shows the same property list, but this time it is opened by selecting the <project name>-Info.plist file in the project navigator.
Figure 9.2 Looking inside the property list file
There are two major differences between the two figures. In Figure 9.1, which is part of the display of the project, the property list is embedded in an interface containing other information about the project. In Figure 9.2, you’re looking just at the property list. In addition to the added information in Figure 9.2, look at the very top of the property list: you can see that the entire property list is inside a dictionary called Information Property List. You can experiment if you want to prove that, if you close the Information Property List dictionary, you have only a single item in the property list.
The fact that the property list as formatted in Figure 9.1 seems to have a number of separate entries and the same property list as formatted in Figure 9.2 clearly shows a root-level container (the dictionary) matters for iCloud synchronization using KVC. Because the array and dictionary collection classes are eligible for KVC, synchronizing a property list as complex as this one with several layers of embedded data is exactly the same process you saw in Chapter 8, “Using Key-Value Coding (KVC).”
Specifically, you retrieved a value from the store with this code:
_editedValue = [[NSUbiquitousKeyValueStore defaultStore]
stringForKey:kEditableField];
You set the value with this code:
[[NSUbiquitousKeyValueStore defaultStore]setString:
self.editedValue forKey:kEditableField];
Thus, you already know how to store and retrieve a property list to and from the ubiquitous KVC store.
Looking Inside a Property List
There is more to consider as you look at the property list shown in Figures 9.1 and 9.2. If you open it with a text editor such as BBEdit, you can see what is actually inside the file. It is shown in Listing 9.1. As you can see, it is XML. When Xcode displays the property list as part of a project’s details, it formats it as shown in Figure 9.1. When a file with the plist extension is opened in Xcode, it is formatted as shown in Figure 9.2. And when you open the file using a text editor, you can see the XML shown in Listing 9.1.
Listing 9.1 Info Property List File
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleDisplayName</key>
<string>${PRODUCT_NAME}</string>
<key>CFBundleExecutable</key>
<string>${EXECUTABLE_NAME}</string>
<key>CFBundleIdentifier</key>
<string>com.champlainarts.${PRODUCT_NAME:rfc1034identifier}</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>${PRODUCT_NAME}</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>1.0</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>UIMainStoryboardFile</key>
<string>MainStoryboard_iPhone</string>
<key>UIMainStoryboardFile~ipad</key>
<string>MainStoryboard_iPad</string>
<key>UIRequiredDeviceCapabilities</key>
<array>
<string>armv7</string>
</array>
<key>UIStatusBarTintParameters</key>
<dict>
<key>UINavigationBar</key>
<dict>
<key>Style</key>
<string>UIBarStyleBlack</string>
<key>Translucent</key>
<false/>
</dict>
</dict>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
</dict>
</plist>
In practice, you almost never look at the XML representation of a property list. And, for preferences or settings, you commonly use the user defaults API described in the following section. Property lists have been a part of Cocoa from its very earliest days, which means that the code used to read and write them has been tested and optimized over time. (Yet, even today, there are some people who believe that the overhead of converting from a text-based format like XML to objects in memory adversely affects performance. Most people disagree except for very large property lists and very slow processors.)
Another consequence of the long history of property lists is that if you search for information on the web, you may get misleading results. Today, you can store property lists in XML or in binary. Many years ago, another format (ASCII) was available, but it has not been supported for years. If you consider that KVC ubiquitous storage for a single app is limited to 1 MB, you can get an idea for a reasonable upper limit of a property list’s size. Yes, not every property list needs to fit into KVC ubiquitous storage, but if you observe that limit, you won’t have to remember which of your property lists are eligible for iCloud and which aren’t.
Reading and Writing Property Lists
Because property lists are usually dictionaries or arrays (at least in terms of their root object), those are the methods you use to read and write them. Both NSDictionary and NSArray have these instance methods:
- writeToFile:atomically
- writeToURL:atomically
There are class methods to read the data back in. NSDictionary has the following class methods:
+ dictionaryWithContentsOfFile:
+ dictionaryWithContentsOfURL:
For NSArray, here are the relevant class methods:
+ arrayWithContentsOfFile:
+ arrayWithContentsOfURL:
There are other ways of accomplishing the same goal (such as alloc followed by a class init method).
Using NSData Objects in Property Lists
An NSData object can be a binary representation of anything. By using NSData objects in a property list, you can take an object in your app and serialize it for storage in a property list. Apple advises against using NSData objects in ubiquitous KVC stores because they can become quite large. If they do become large, the overhead of synchronizing them across a user’s devices can be serious.
It’s not that they don’t work, it’s that the temptation to use them may be too great. If you want to save an app’s state and synchronize it, before you use an NSData object, consider whether Core Data (the topic of Chapter 10, “Managing Persistent Storage with Core Data”) is a better solution.
Using Scalars in Property Lists
There is one further point to remember about property lists. You can store scalars in them, but you need to convert the scalar to one of the property list types—usually an NSNumber. You can use the NSNumber class methods to do this. These are the class methods to use:
+ numberWithBool:
+ numberWithChar:
+ numberWithDouble:
+ numberWithFloat:
+ numberWithInt:
+ numberWithInteger:
+ numberWithLong:
+ numberWithLongLong:
+ numberWithShort:
+ numberWithUnsignedChar:
+ numberWithUnsignedInt:
+ numberWithUnsignedInteger:
+ numberWithUnsignedLong:
+ numberWithUnsignedLongLong:
+ numberWithUnsignedShort:
Working with User Defaults
The user defaults system wraps up the technologies described earlier in this chapter so that you can manage them at a high level of abstraction. The fact that property lists and property-list objects are at the heart of the system does not normally affect the way in which you use the user defaults system as a developer (or the ways in which users use it).
As a developer, you decide what defaults you want to use. There are four key issues to consider in setting up your defaults:
Can the user set them?
How frequently are they changed?
Where should the defaults and settings be located—inside the app or in System Preferences (OS X) or Settings (iOS)?
How do you use iCloud with them?
Can the User Set Defaults?
When it comes to a default such as the default font to use for new documents (if you have such a default), you may want to make this settable by the user. The more defaults a user can set, the more the user’s experience with your app can be enhanced. On the other hand, the more customization you allow, the more complex the app can become for both the developer and user. Depending on your app, you need to find a comfort area for both yourself and your users. (You may also be thinking about the difference between a Lite and a Pro version of your app.)
In the case of a hypothetical default for a font in new documents, you have three basic choices:
You can make the default your choice and allow users to change fonts as they want, but they would always start from your choice.
You can let users set the default.
You can recast the question to provide a simpler user interface if possible. For example, in this case you could use the font of the previous document. Creating a new document and setting its font would reset the unseen preference. (For documents that allow multiple fonts, you would have to make some decisions striking the balance between ease of use and mysterious behavior.)
How Frequently Are Defaults Changed?
Some defaults are set frequently, while others are set-and-forget defaults. Sometimes, they work in pairs. For example, in an app that presents text, movies, or music, you might want to automatically store the last location the user has visited. On the other hand, you might want to always start from a known location (the beginning, perhaps). You can have it both ways by having a user-settable default to allow users to choose which strategy they prefer. Then you can pair it, if necessary, with a default that is the last location.
Where Should the Defaults and Settings Be Located?
Should the defaults and settings be located inside the app or in System Preferences (OS X) or Settings (iOS)?
Users are used to two basic UI approaches for defaults: control from within your app or control from another app. If you implement control of defaults from within your app on OS X, users will expect to find it in a Preferences command in the app menu. What you place there is up to you.
On iOS, a common icon to open a preferences UI is the gear wheel. Again, the layout of the interface is up to you. (See “Registering Defaults” later in this chapter for the code you’ll use to implement the defaults.)
On OS X, you can add a pane to System Preferences to let users manage your app’s preferences. In a similar vein, you can use a Settings bundle on iOS so that users manage your defaults with Settings.
How Do You Use iCloud with Your User Defaults?
This section walks you through some of the synchronizing concerns that you have to consider with iCloud. This is pretty much the process you have to go through with any sync strategy. It’s a step-by-step process in which you look at every permutation and variation that you can think of. You may think you should be writing code, but without this type of painstaking analysis, your code won’t be very usable.
As you saw in Chapter 8, you can use the KVC ubiquity store to keep track of defaults. Listing 8.3 showed you how to load existing defaults on iOS, and Listing 8.4 showed you how to load existing defaults on OS X. Thereafter, you monitor changes in iCloud as shown in Listing 8.5.
Defaults that are device-specific are usually poor candidates for iCloud, but behaviors that provide a comparable user experience across devices are good candidates. What is not handled in Chapter 8 is what you do if the values are inconsistent. One approach is to load the local values as shown in Chapter 8, and then overwrite them with iCloud values if they exist. This has the virtue of getting a default value in place as quickly as possible, but the cost is that a default that has been changed on another device may take longer to be set. Furthermore, if iCloud is unavailable now or when the other device changed the default, you can wind up with a value that is not the most recent value that the user set.
If the values are inconsistent, you may be able to add logic to resolve the problem. The classic example given is a default value that represents the user’s highest score in a game that can be played on any of the devices for that iCloud account. That value should only go up over time, so the automatic choice may be the higher of two inconsistent values.
Of course, you may allow the user to reset the highest score. Factoring that issue into the mix means that the value you want would be either the highest value or zero, which would be the default value for the case in which no score has yet been achieved—the case of a reset to the highest score.
Does that sound right? Is your default synchronization strategy complete?
The flaw in the logic is that you can’t distinguish between a zero that represents a reset of the highest score and a zero that represents a new device. Sometimes with iCloud you do have to ask the user to step in. It is a best practice to minimize user intervention with iCloud synchronization, but if you can formulate a simple question to pose in an alert, let the user choose.
Registering Defaults
As you saw in Chapter 8, you typically get to your app’s defaults by using the NSUserDefaults class method:
myDefaults = [NSUserDefaults standardUserDefaults];
Once you have it, you can then set or get any individual defaults. If no default value has been set yet, zero or nil is returned when you ask for the value. This is often not the best choice. The best way to handle default defaults (that is, default values that the user has not set) is to useregisterDefaults: when your app starts up. Pass in a dictionary with keys and values to use as the defaults for unset defaults:
- (void)registerDefaults:(NSDictionary *)dictionary
Thus, in applicationDidFinishLaunchingWithOptions: (iOS) or applicationDidFinishLaunching: (OS X), you might use code such as that shown in Listing 9.2. Note that Listing 9.2 uses the literals syntax introduced in Xcode 4.4 (OS X) and Xcode 4.5 (iOS). One of the advantages of this syntax is that the order of the keys and values are reversed from the NSDictionary class method dictionaryWithObjectsAndKeys:. Now you don’t have to wonder which comes first—the key or the value. Keys come first just as they do in the name: key-value coding.
Listing 9.2 Registering Default Values
// Insert code here to initialize your application
NSDictionary *defaultDefaults =@{
@"saveIntervalInMinutes" : @"10",
@"playerName" : @"Player",
@"opponentName" : @"Computer"
};
[[NSUserDefaults standardUserDefaults] registerDefaults: defaultDefaults;
Chapter Summary
In this chapter, you have seen how property lists and property-list objects implement settings and preferences. The user defaults system provides a high-level interface to them that you can use to synchronize settings where appropriate across devices. You have seen the issues you must consider in providing a consistent and logical customization environment for your users as well as when you should consider iCloud synchronization.
Exercises
1. If you haven’t used the new literals syntax shown in Listing 9.2, this is a good time to bring yourself up to speed. It’s a simpler way of building arrays and dictionaries than the previous technique. Using the Xcode Organizer or developer.apple.com, check out the comparable syntax forNSArray.
2. For the types of apps that you are most interested in (games, productivity, or whatever) read the reviews on the App Store and search for comments on the web. Pay particular attention to the comments, suggestions, and complaints about settings. You’ll find plenty of them in many cases. Use your research as you plan your settings and preferences. What do people really like or dislike? What annoys them?
3. Try out your proposed settings and preferences on users. As developers, we tend to understand the settings and preferences in a different way from users.