Use Your Loaf

[[brain engage] write]

Sync Preference Data With iCloud

One of the most interesting additions to iOS 5 was iCloud storage which makes it easy to sync data between apps running on different iOS and OS X devices. Effectively you can store key application data in the cloud and have it propagate to all instances of the app wherever they are running. The iCloud storage services supports two very different data models:

  • document storage
  • key value storage

In this post I will focus on using the key value store to sync a small amount of user preference data. This is the simplest and quickest way to start to use iCloud storage. I will follow up on using the document model and core data in future posts.

When to use Key-value Storage

The key-value store is intended to be used for small amounts of data, typically user preferences or other configuration data that does not change too frequently. It is not intended for syncing large amounts of user data such as documents or user created data. Each application is limited to a maximum of 64 KB 1 MB of total key-value storage which also means individual keys cannot exceed 64 KB 1 MB of storage. An application is also limited to a total of 1024 keys.

It should also be noted that the iCloud key-value store is not intended to replace local user preference data. The local preference data is needed for when the device is offline or whenever the iCloud store is not accessible.

The Sample App - SyncMe

To illustrate how easy it is to add iCloud key value storage to an app I will adapt a simple standalone app to sync a configuration item stored as a user preference between instances of the app running on multiple iOS devices. The user interface for the app is shown below:

The basic idea is that touching one of the buttons changes the background colour of the view. The selected colour is stored as a user preference making it suitable for storing in the iCloud key-value store. Changing the colour in a running instance of the app should cause the preference and hence the background colour to be updated in all other instances of the app.

The app was built from the Xcode single view application template and is very simple so I will just summarise the key points before we start to look at what is required to add iCloud support.

The user interface is a single view with six UIButtons corresponding to the six possible background colours. The Touch Up Inside action of each button is connected to a single method in the view controller (UYLViewController) responsible for storing the selected colour in the user preferences and then actually changing the background colour of the view.

Each button has a tag value set in Interface Builder which corresponds to an enumerated type defined in the view controller as follows:

typedef enum { UYLblack, UYLblue, UYLgreen, UYLpurple, 
               UYLred, UYLyellow } UYLcolor;

So for example the tag setting for the yellow button is as follows:

The method in the view controller which is connected to the button actions is shown below:

- (IBAction)colourChange:(id)sender {

  UYLcolor colorTag = [sender tag];
  NSNumber *color = [NSNumber numberWithInteger:colorTag]; 

  NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
  [defaults setObject:color forKey:kUYLKVStoreBackgroundColorKey];

  [self changeBackgroundColor];
}

This method retrieves the tag from the UIButton which triggered the method call and after wrapping the value in an NSNumber object stores it in the apps local preferences store using the NSUserDefaults class. For more details on using NSUserDefaults you may want to take a look at Adding a settings bundle to an iPhone App (note that in this case we are not actually going to use a settings bundle as we do not need the user interface provided by the settings application).

The key used to store the preference is defined as an NSString in the class implementation:

NSString *kUYLKVStoreBackgroundColorKey = @"backgroundColor";

Finally the method calls changeBackgroundColor to actually change the view background colour. It does this by setting the backgroundColour property of the view to the corresponding colour after retrieving the key from the user preferences:

- (void)changeBackgroundColor {

  NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
  UYLcolor color = [defaults integerForKey:kUYLKVStoreBackgroundColorKey];

  switch (color) {
    case UYLblue:
      self.view.backgroundColor = [UIColor blueColor];
      break;
    case UYLgreen:
      self.view.backgroundColor = [UIColor greenColor];
      break;
    case UYLpurple:
      self.view.backgroundColor = [UIColor purpleColor];
      break;
    case UYLred:
      self.view.backgroundColor = [UIColor redColor];
      break;
    case UYLyellow:
      self.view.backgroundColor = [UIColor yellowColor];
      break;
    default:
      self.view.backgroundColor = [UIColor blackColor];
      break;
  }
}

The final piece of code in the view controller is to ensure we set the view background colour when the view first loads. We can do that by calling the changeBackgroundColor method from viewDidLoad:

- (void)viewDidLoad
{
  [super viewDidLoad];
  [self changeBackgroundColor];
}

Of course this assumes we have set a default for the preference to cover the situation when the app runs for the first time. Actually this is not strictly necessary this time as an integer value will default to zero if it is not set in the user preferences which will give us a black background. However just to be explicit we can register a default value in the application delegate:

- (BOOL)application:(UIApplication *)application
        didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
  NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
  NSDictionary *appDefaults = [NSDictionary
                dictionaryWithObject:[NSNumber numberWithInteger:0]
                              forKey:kUYLKVStoreBackgroundColorKey];
  [defaults registerDefaults:appDefaults]; 
  ...
  ...
  return YES;
}

So that is the app without iCloud support. Changing the background colour updates the locally stored user preference so that we maintain the same user selected colour each time the app runs. However if the app is running on multiple iOS devices the user must set the colour preference on each app since the local user preferences are not synchronised between devices.

Managing Entitlements

To get started with iCloud Key-value storage we must first modify the app entitlements file to add the com.apple.developer.ubiquity-kvstore-identifier. The easiest way to do that is from the Summary screen of the target in Xcode. Enabling the entitlements option will provide default values for the iCloud Key-Value Store identifier as well as the iCloud Containers key which is used when you are using iCloud document storage.

The default values are based on the application bundle identifier. If you are creating multiple versions of an app (e.g. a free and premium version or separate iPhone and iPad versions) you should use the same identifier in all versions of the app if you want them to all share the same store.

Saving Preferences to iCloud

With the entitlements correctly configured we can now save the user preferences to the iCloud Key-value store anytime we update the locally stored preferences. The code is very similar except that instead of using NSUserDefaults we use NSUbiquitousKeyValueStore. So for example the colourChange method used to update the local preferences now becomes:

- (IBAction)colourChange:(id)sender {

  UYLcolor colorTag = [sender tag];
  NSNumber *color = [NSNumber numberWithInteger:colorTag];

  NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
  [defaults setObject:color forKey:kUYLKVStoreBackgroundColorKey];

  NSUbiquitousKeyValueStore *store = [NSUbiquitousKeyValueStore defaultStore];
  if (store) {
    [store setObject:color forKey:kUYLKVStoreBackgroundColorKey];
  }
  [self changeBackgroundColor];
}

The defaultStore method returns us the shared key-value store object which we can then use to store the preference. Note that we are using the same key when storing the value in the key-value store that we use when storing in the local preference file. This is not required but it makes things easier when processing updates as we will see shortly.

Also note that we check we are actually returned the shared store object which ensures this code will also run on pre-iOS 5 versions of iOS. On earlier versions of iOS which do not support iCloud this code will not return a key-value store and hence is harmless. Since NSUbiquitousKeyValueStore is part of the Foundation framework you do not need to worry about weak-linking the framework since it is always present and the LLVM compiler takes care of weak-linking the class for us.

In this case we are only updating a single value. In situations where we need to update multiple values it is also possible to use an NSDictionary to set all values in a single operation. This ensures that either all values are updated together or none of them are updated. The only reason why the update might fail that is currently defined is if we exceed the 64 KB quota for the key-value store though that can of course change in the future.

Handling Updates from iCloud

To be notified whenever a key value store is modified or when we need to initially sync to iCloud we need to register for the NSUbiquitousKeyValueStoreDidChangeExternalNotification notification. It is best to do that as soon as the app is loaded so we will add the necessary code to the viewDidLoad method:

- (void)viewDidLoad
{
  [super viewDidLoad];
  [self changeBackgroundColor];

  NSUbiquitousKeyValueStore* store = [NSUbiquitousKeyValueStore
                                      defaultStore];
  if (store) {
    [[NSNotificationCenter defaultCenter] addObserver:self
      selector:@selector(storeChanged:)
      name:NSUbiquitousKeyValueStoreDidChangeExternallyNotification
      object:store];
    [store synchronize];
  }
}

Anytime the iCloud Key-value store is modified we will get a call to the storeChanged method which can then process the change as follows:

- (void)storeChanged:(NSNotification*)notification {

  NSDictionary *userInfo = [notification userInfo];
  NSNumber *reason = [userInfo 
                    objectForKey:NSUbiquitousKeyValueStoreChangeReasonKey];

  if (reason) {
    NSInteger reasonValue = [reason integerValue];
    NSLog(@"storeChanged with reason %d", reasonValue);

    if ((reasonValue == NSUbiquitousKeyValueStoreServerChange) ||
        (reasonValue == NSUbiquitousKeyValueStoreInitialSyncChange)) {

      NSArray *keys = [userInfo
                     objectForKey:NSUbiquitousKeyValueStoreChangedKeysKey];
      NSUbiquitousKeyValueStore *store = [NSUbiquitousKeyValueStore defaultStore];
      NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];

      for (NSString *key in keys) {
        id value = [store objectForKey:key];
        [userDefaults setObject:value forKey:key];
        NSLog(@"storeChanged updated value for %@",key);
      }

      [self changeBackgroundColor];
    }
  }
}

The userInfo dictionary contained in the notification contains two objects of interest. The first, with key NSUbiquitousKeyValueStoreChangeReasonKey, provides the reason why the change notification was generated. There are two reasons that we are interested in:

  • NSUbiquitousKeyValueStoreServerChange: this value indicates that the key-value store has changed in the cloud most likely because another device has sent a new value.
  • NSUbiquitousKeyValueStoreInitialSyncChange: this value occurs the first time the app runs and has yet to be synced with iCloud.

In both of these cases we can retrieve the second object in the userInfo dictionary with key NSUbiquitousKeyValueStoreChangedKeysKey. This object is an NSArray containing the keys of all objects that have been changed. These changed objects can then be retrieved from the defaultStore and written to the local user preferences using the traditional NSUserDefaults object. Since we are using the same key to store values in iCloud and in the local preferences file we can take a shortcut by looping through each of the keys and use the setObject:forKey method to write to the local preferences file. Finally with the change stored locally we call the changeBackgroundColor method to actually change the view background colour.

There is one more reason that can currently be returned in the notification. NSUbiquitousKeyValueStoreQuotaViolationChange indicates that we have exceeded the 64 KB limit of the key-value store. In this example I ignore this error but you could log the error or I suppose report it to the user. My guess is that if you are in danger of exceeding the 64 KB limit you are probably using the wrong approach.

Provisioning the App

To actually test iCloud storage you will need to deploy it to an actual device - as far as I can tell it will not work in the simulator. Before you deploy you will need to generate a provisioning profile (or update an existing profile) to use an App ID that is enabled for iCloud:

Testing the Sync

Once you have the app correctly provisioned we can deploy it to several iOS devices and see how well it works. The first iOS device we run the app on will write values to the store once we start changing the colour. If we then run the app on a second device the background colour starts as black (the default local preference setting) and then after a few seconds we receive the first notification (actually an NSUbquitousKeyValueStoreInitialSyncChange) and the colour changes to the choice we made on the first device.

In practise you will find that it can take anywhere from a few seconds to more than a minute for changes to propagate. The iCloud guidelines from Apple do indicate that the Key-value store is intended for small amounts of configuration data that are not expected to change frequently. In fact they even imply that if you make frequent calls to the store this will slow down future updates:

If the apps on a device make frequent changes to the key-value store, the system may defer the synchronization of some changes in order to minimize the number of round trips to the server. The more frequently apps make changes, the more likely it is that later changes will be deferred and not show up on other devices right away.

Wrapping Up

I hope you have found this initial look at iCloud useful. The key-value store is the easiest iCloud access method to add to an App and I expect many apps to start to make use of it to sync user preferences. As always the full Xcode project for the example app can be downloaded here or browsed in my public Github repository.

Comments