Designing for Key-Value Data in iCloud

To store discrete values in iCloud for app preferences, app configuration, or app state, use iCloud key-value storage. Key-value storage is similar to the local user defaults database; but values that you place in key-value storage are available to every instance of your app on all of a user’s various devices. If one instance of your app changes a value, the other instances see that change and can use it to update their configuration, as depicted in Figure 2-1.

Figure 2-1  iCloud key-value storage

Zooming in on the key-value storage interactions for a single device, as shown in Figure 2-2, you can see that your app accesses key-value storage by employing the shared NSUbiquitousKeyValueStore object.

Figure 2-2  iCloud key-value storage access

As you do with an NSUserDefaults object, use the iCloud key-value store to save and retrieve scalar values (such as BOOL) and property-list object types: NSNumber, NSString, NSDate, NSData, NSArray, and NSDictionary. Array and dictionary values can hold any of these value types.

The NSUbiquitousKeyValueStore class provides methods for reading and writing each of these types, as described in NSUbiquitousKeyValueStore Class Reference.

Each time you write key-value data, the operation succeeds or fails atomically; either all of the data is written or none of it is. You can take advantage of this behavior when your app needs to ensure that a set of values is saved together to ensure validity: place the mutually-dependent values within a dictionary and call the setDictionary:forKey: method.

Enable Key-Value Storage

Before you can use key-value storage, your app must have the appropriate entitlement, as explained in “Request Access to iCloud Storage By Using Entitlements.” And that is all the setup you need.

Prepare Your App to Use the iCloud Key-Value Store

Any device running your app, and attached to a user’s iCloud account, can upload key-value changes to that account. To keep track of such changes, register for the NSUbiquitousKeyValueStoreDidChangeExternallyNotification notification during app launch. Then, to ensure your app starts off with the newest available data, obtain the keys and values from iCloud by calling the synchronize method. (You need never call the synchronize method again during your app’s life cycle, unless your app design requires fast-as-possible upload to iCloud after you change a value.)

The following code snippet shows how to prepare your app to use the iCloud key-value store. Place code like this within your application:didFinishLaunchingWithOptions: method (iOS) or applicationDidFinishLaunching: method (OS X).

// register to observe notifications from the store
[[NSNotificationCenter defaultCenter]
    addObserver: self
       selector: @selector (storeDidChange:)
           name: NSUbiquitousKeyValueStoreDidChangeExternallyNotification
         object: [NSUbiquitousKeyValueStore defaultStore]];
 
// get changes that might have happened while this
// instance of your app wasn't running
[[NSUbiquitousKeyValueStore defaultStore] synchronize];

In your handler method for the NSUbiquitousKeyValueStoreDidChangeExternallyNotification notification, examine the user info dictionary and determine if you want to write the changes to your app’s local user defaults database. It’s important to decide deliberately whether or not to change your app’s settings based on iCloud-originated changes, as explained next.

Actively Resolve Key-Value Conflicts

The implementation of iCloud key-value storage makes conflicts unlikely, but you must design your app to handle them should they occur. This section describes a key-value conflict scenario and explains how to handle it.

When a device attached to an iCloud account uploads a value to key-value storage, iCloud overwrites the corresponding, older value on the server. To determine which of two competing instances of a value is the newest, iCloud compares their universal-time timestamps as provided by the devices that uploaded the values. Usually, this simple model provides what a user wants.

However, it may be the case that an older value, not its replacement, newer one, is the “correct” value. It is your app’s responsibility to Identify and resolve this situation. To allow your app to do that, use the NSUbiquitousKeyValueStore object in concert with—not as a replacement for—user defaults.

For example, say you develop a game that keeps track of the highest level achieved, and a user has reached level 13 on their iPhone—shown as the first step in the Device A column in Figure 2-3.

Figure 2-3  Creation of a key-value conflict

The user then opens your app for the first time on an iPad (shown as the first step in the Device B column in the figure) that is not currently connected to iCloud. So, the initial conditions shown in the figure are as follows:

Continuing with this example, the user then connects the iPad to iCloud, shown as the “Device goes online” event in the Device B column in the figure; the device uploads the level-achievement value of 1.

iCloud sees that the most recent level-achievement value is 1, and, because it is newer, overwrites the previous value of 13. iCloud then pushes the value 1 to all connected devices. As shown in the final step in the Device A column of Figure 2-3, this is certainly not what the user wants. And this is where your app design comes in.

When your app’s state or configuration changes (such as, in this example, when the user reaches a new level in the game), first write the value to your user defaults database using the NSUserDefaults class. Then compare that local value to the value in key-value storage. If the values differ, your app needs to resolve the conflict by picking the appropriate winner.

In this example, you have stored the user’s true highest level reached—13—in user defaults on Device A; so your app has the information it needs to do what the user expects. When the instance of your app on Device A receives an NSUbiquitousKeyValueStoreDidChangeExternallyNotification notification, compare the (lower) achievement value that it sees as newly-downloaded from iCloud, with the (higher) value in user defaults. Recognizing that in a level-based game, a user wants to keep building on their highest level achieved, write the true highest level (13 in this example) to key-value storage. iCloud then overwrites the value of 1 with 13 on the iCloud server, and then pushes that (true) highest achievement level to Device B. The iPad’s app instance should then recognize the pushed value of 13 as appropriate, and should overwrite the local user defaults achievement value of “1” with “13.”

Data Size Limits for Key-Value Storage

The total space available in your app’s iCloud key-value storage is 1 MB per user. The maximum number of keys you can specify is 1024, and the size limit for each value associated with a key is 1 MB. For example, if you store a single large value of exactly 1 MB for a single key, that fully consumes your quota for a given user of your app. If you store 1 KB of data for each key, you can use 1,000 key-value pairs.

The maximum length for a key string is 64 bytes using UTF8 encoding. The data size of your cumulative key strings does not count against your 1 MB total quota for iCloud key-value storage; rather, your key strings (which at maximum consume 64 KB) count against a user’s total iCloud allotment.

If your app has exceeded its quota in key-value storage, the iCloud key-value store posts the NSUbiquitousKeyValueStoreDidChangeExternallyNotification notification with a value of NSUbiquitousKeyValueStoreQuotaViolationChange in its user info dictionary.

For more on how to use iCloud key-value storage in your app, see “Storing Preferences in iCloud” in Preferences and Settings Programming Guide, and refer to NSUbiquitousKeyValueStore Class Reference.

Exercise Care When Using NSData Objects as Values

Using an NSData object lets you store arbitrary data as a single value in key-value storage. For example, in a game app, you can use it to upload complex game state to iCloud, as long as it fits within the 1 MB quota.

However, a data object in iCloud is available to be read, modified, and written by every instance of your app on a device attached to a user’s account. Some of these instances could be older versions or running on another platform. Because of this, your data format requires the same careful design and care in handling as you would apply when using document storage, as explained in “Design for Robustness and Cross-Platform Compatibility.”

In addition, the larger your data objects, the more data gets uploaded to iCloud, and down to each attached device, each time you make a change.

For these reasons, Apple recommends that you employ other property list objects, such as NSDictionary, for your key-value storage values if possible.

Don’t Use Key-Value Storage in Certain Situations

Every app submitted to the App Store or Mac App Store should adopt key-value storage, but some types of data are not appropriate for key-value storage. In a document-based app, for example, it is usually not appropriate to use key-value storage for state information about each document, such as current page or current selection. Instead, store document-specific state, as needed, with each document, as described in “Design for Persistent Document State.”

In addition, avoid using key-value storage for data essential to your app’s behavior when offline; instead, store such data directly into the local user defaults database.