Keeping application data synchronized across devices is a complex and daunting task. Fortunately, that is exactly why Apple built iCloud. In this Tuts+ Premium series, you will learn how iCloud works and how your applications can seamlessly share data across multiple devices.
Also available in this series:
- Working with iCloud: Introduction
- Working with iCloud: Key-Value Storage
- Working with iCloud: Document Storage
- Working with iCloud: Core Data Integration
In the first installment of this series, I gave an introduction to iCloud and focused on iCloud Storage in particular. Two types of iCloud Storage are avaialable to developers, Key-Value Storage and Document Storage. Key-Value Storage will be the focus of this tutorial.
We will start this tutorial by taking a closer look at the process of setting up an application for use with iCloud. With our application properly configured, we will make use of iCloud's Key-Value Storage to keep our application's data synchronized across multiple devices.
Before We Begin
We cannot test data synchronization with just one device. To complete this tutorial, you will need to have at least two iOS devices running iOS 5 or higher. Unfortunately, the iOS Simulator cannot be used to test iCloud Storage.
The application we are about to build will be a simple iOS application, which means that you will be able to run it on any iPhone, iPod Touch, or iPad running iOS 5 or higher. Reading this tutorial will still be worthwhile even if you don't have two separate iOS devices to use for testing. It will teach you how iCloud is set up and how you can use iCloud's Key-Value Storage to enhance your own applications.
Step 1: Project Setup
I will start by walking you through the process of configuring our application to use iCloud Storage. However, we first need to set up our Xcode project.
Create a new project in Xcode by selecting the Single View Application template. Name your application Cloudy, enter a company identifier, set iPhone for the device family, and check Use Automatic Reference Counting. The remaining checkboxes should be unchecked. Tell Xcode where you want to save your project and hit Create.
It is key that you carefully choose the product name and company identifier for this project. The combination of these two elements form the bundle identifier (together with the bundle seed identifier) of your application, which iCloud will use to uniquely identify your application. Carefully check your spelling, because the bundle identifier is case sensitive.
Step 2: Setting Up iCloud
As I mentioned in the first article of this Tuts+ Premium series, setting up an application to use iCloud is easy and involves only two steps. Let me walk you through the process step-by-step.
Step 2A: Provisioning Portal
Open your favorite browser and head over to Apple's iOS Dev Center. Log in to your iOS developer account and click the iOS Provisioning Portal link on the right.
First, we need to create an App ID for our application. Open the App IDs tab on the left and click the New App ID button at the top right. Give your App ID a descriptive name to easily identify it later. Next, enter the bundle identifier I talked about a few moment ago. The bundle identifer is the combination of your company identifier, com.mobiletuts in our example, and the product name, Cloudy in our example. You can leave the bundle seed identifier set to Use Team ID, which is fine for our application.
Double-check that your bundle identifier is spelled correctly. As I mentioned earlier, the bunder identifier is case sensitive. Click the submit button to create your App ID.
Your newly created App ID should now be present in the list of App IDs. You will notice that iCloud is disabled by default for every newly created App ID. Let's change that. On the right of your App ID, click the Configure button. At the bottom of the page, you should see a checkbox that says Enable for iCloud. Check the checkbox and click Done. Our App ID is now configured to work with iCloud. There is one more thing we need to take care of while we are in the Provisioning Portal.
To ensure that our application can run on our devices, we need to create a provisioning profile. Still in the iOS Provisioning Portal, open the Provisioning tab on the left and select the Development tab at the top. Click the New Profile button at the top right and enter a descriptive name for your new profile. Next, select the development certificate you want the profile to be associated with and choose the correct App ID from the drop-down list. Finally, select the devices you will use for testing from the list at the very bottom of the page.
After clicking the submit button in the bottom right, you will notice that your provisioning profile has a status of Pending. After reloading the page, the profile's status should have updated to Active. Click the download button next to your provisioning profile and double-click the profile. Xcode will automatically install the profile for you.
Step 2B: Entitlements
Many developers cringe when they hear the word entitlements. Entitlements are nothing to be scared of once you understand what they are for. Entitlements grant specific capabilities or security permissions to an application. That is all there is to it. It is a simple list of key-value pairs telling the operating system what your application can and cannot do, and which applications have access to your application's data. The latter is especially important when working with iCloud.
In our example, iCloud entitlements let us enable iCloud Storage for our application. To configure the entitlements of our application, we head over to Xcode's target editor.
Click our project in the Project Navigator and select our target from the targets list. With the Summary tab selected, you should see a section named Entitlements at the bottom. Check the first checkbox to enable entitlements for our application. This option will create an entitlments file for our application. The text field below the checkbox should be prefilled for you. We now have the option to enable iCloud Key-Value Storage and iCloud Document Storage.
In this tutorial, I will only talk about iCloud Key-Value Storage so check the checkbox next to iCloud Key-Value Store. Again, Xcode prefills the text field next to the checkbox with your application bundle identifier (without the bundle seed identifier). Verify that this bundle identifier corresponds to the one you entered in the provisioning portal when you created your App ID. I will talk about iCloud Containers in the next tutorial as they relate to iCloud Document Storage.
At the bottom of the entitlements section, you see Keychain Access Groups. This list specifies which applications have access to your applications keychain items. You can leave that section untouched for now.
Before we get our hands dirty with the iCloud API's, we need to configure our build settings. Still in Xcode's target editor, select the Build Settings tab and scroll down to the Code Signing section. Set the code signing identity for the Debug and Release configurations to match the profile we just created in the iOS Provisioning Portal.
Step 3: User Interface
This tutorial is somewhat advanced and I therefore assume that you are familiar with the basic concepts of iOS development. This means that I will move a bit faster than I usually do since we have a lot of ground to cover.
The application that we are about to build will be a simple bookmarks manager. We can add and delete bookmarks, and our bookmarks will be synced across our devices. How cool is that? Let's get started.
Open our view controller's header file and create an instance variable (ivar) of type NSMutableArray
with a name of bookmarks. The ivar will store our bookmarks. The bookmarks will be displayed in a table view. Create an outlet for the table view in our view controller's header file and make sure to conform our view controller to the table view data source and delegate protocols. Also, don't forget to synthesize accessors for both properties in our view controller's implementation file. Next, add two actions to ViewController.h, editBookmarks: and addBookmark:. Ready? Head over to the ViewController.xib file to wire everything up.
#import <UIKit/UIKit.h> @interface ViewController : UIViewController <UITableViewDataSource, UITableViewDelegate> { NSMutableArray *_bookmarks; __weak UITableView *_tableView; } @property (nonatomic, strong) NSMutableArray *bookmarks; @property (nonatomic, weak) IBOutlet UITableView *tableView; - (IBAction)editBookmarks:(id)sender; - (IBAction)addBookmark:(id)sender; @end
Start by dragging a navigation bar to our view controller's view and position it at the very top. Drag an instance of UIBarButtonItem
to the left of the navigation bar and set its identifier in the Attributes Inspector to Edit. Control drag from the edit button to our File's Owner and select the editBookmarks: method. Drag a second bar button item to the right of the navigation bar and give it a identifier of Add. Control drag from the add button to our File's Owner and select the addBookmark: method. Finally, drag an instance of UITableView
to our view controller's view and connect its data source and delegate outlets to the File's Owner object. Done.
The implementation of our table view's data source and delegate protocols is simple and straightforward. In the numberOfSectionsInTableView: method, we verify whether our data source, that is, our bookmarks, is not nil. If it is nil, we return 1. If isn't, we return 0. The tableView:numberOfRowsInSection: method is almost identical. Instead, if our data source is not nil, we return the number of elements it contains. If our data source is nil, we return 0.
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { if (self.bookmarks) { return 1; } else { return 0; } } - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { if (!self.bookmarks) { return 0; } else { return self.bookmarks.count; } }
In the tableView:cellForRowAtIndexPath: method, we start by declaring a static cell identifier for the purpose of cell reuse and then ask the table view for a reusable cell with the cell identifier we just declared. If a reusable cell was not returned, we initialize a new cell with a style of UITableViewCellStyleSubtitle
and we pass our cell identifier for cell reuse. Next, we fetch the correct bookmark from the data source. Each bookmark will be a dictionary with two key-value pairs, a name and a URL. We configure the cell by setting its label and detail label with the bookmark's name and url, respectively.
- (UITableViewCell *)tableView:(UITableView *)aTableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { static NSString *CellIdentifier = @"Cell Identifier"; UITableViewCell *cell = [aTableView dequeueReusableCellWithIdentifier:CellIdentifier]; if (cell == nil) { cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:CellIdentifier]; } // Fetch Bookmark NSDictionary *bookmark = [self.bookmarks objectAtIndex:indexPath.row]; // Configure Cell cell.textLabel.text = [bookmark objectForKey:@"name"]; cell.detailTextLabel.text = [bookmark objectForKey:@"url"]; return cell; }
The user also has the option to edit their bookmarks. In the tableView:canEditRowAtIndexPath: method we return YES. The tableView:commitEditingStyle:forRowAtIndexPath: is slightly more complex. We start by verifying that the cell's editing style is equal to UITableViewCellEditingStyleDelete
, that is, a bookmark was deleted. First, we update our data source by deleting the bookmark from the bookmarks array. We then save the data source by sending our view controller a saveBookmarks message and we finally update the table view to reflect the changes we made to the data source.
- (BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath { return YES; } - (void)tableView:(UITableView *)aTableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath { if (editingStyle == UITableViewCellEditingStyleDelete) { // Update Bookmarks [self.bookmarks removeObjectAtIndex:indexPath.row]; // Save Bookmarks [self saveBookmarks]; // Update Table View [aTableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade]; } }
Let's take a look at our actions now. The editBookmarks: method couldn't be easier. We toggle the editing state of the table view in an animated fashion.
- (IBAction)editBookmarks:(id)sender { [self.tableView setEditing:!self.tableView.editing animated:YES]; }
The addBookmark: method requires a bit more work. We initialize a new instance of AddBookmarkViewController
, a view controller we will create shortly, and we set it's view controller property to self
. We then present the newly created view controller modally. As its name implies, AddBookmarkViewController
is in charge of creating new bookmarks. Let's take a closer look at AddBookmarkViewController
.
- (IBAction)addBookmark:(id)sender { // Initialize Add Bookmark View Controller AddBookmarkViewController *vc = [[AddBookmarkViewController alloc] initWithNibName:@"AddBookmarkViewController" bundle:[NSBundle mainBundle]]; vc.viewController = self; // Present View Controller Modally [self presentViewController:vc animated:YES completion:nil]; }
Step 4: Adding Bookmarks
The implementation of AddBookmarkViewController
is very straightforward so I won't go into too much detail. The view controller has a weak reference to our main view controller. This allows it to notify our main view controller when a new bookmark has been created. It also has two outlets of type UITextField
, which lets the user enter a name and a url for each bookmark.
#import <UIKit/UIKit.h> @class ViewController; @interface AddBookmarkViewController : UIViewController { __weak ViewController *_viewController; __weak UITextField *_nameField; __weak UITextField *_urlField; } @property (nonatomic, weak) ViewController *viewController; @property (nonatomic, weak) IBOutlet UITextField *nameField; @property (nonatomic, weak) IBOutlet UITextField *urlField; @end
The XIB file contains a navigation bar at the top with a cancel and a save button. The view itself contains the name and url text fields I just mentioned. The cancel and save buttons are connected to a cancel: and a save: action, respectively. The cancel: action simply dismisses our modal view controller. The save: action is just as simple. We create a new bookmark (i.e. an instance of NSDictionary
) with the data the user has entered in the text fields and tell our main view controller about the new bookmark. It is important to note that this isn't the best solution for letting our main view controller know when a new bookmark has been created by our AddBookmarkViewController
. This approach introduces tight coupling, which should be avoided as much as possible. A better approach would be to adopt the delegation pattern.
- (IBAction)cancel:(id)sender { [self dismissViewControllerAnimated:YES completion:nil]; } - (IBAction)save:(id)sender { NSString *name = self.nameField.text; NSString *url = self.urlField.text; NSDictionary *bookmark = [[NSDictionary alloc] initWithObjectsAndKeys:name, @"name", url, @"url", nil]; [self.viewController saveBookmark:bookmark]; [self dismissViewControllerAnimated:YES completion:nil]; }
We are almost ready, but we still need to implement that saveBookmark: method in our main view controller. Start by declaring this method in our view controller's header file and then head over to its implementation file. The implementation is minimal. We add the newly created bookmark to our data source, we save the data source, and then we reload the table view to show the bookmark we just added.
- (void)saveBookmark:(NSDictionary *)bookmark { // Add Bookmark To Bookmarks [self.bookmarks addObject:bookmark]; // Save Bookmarks [self saveBookmarks]; // Reload Table View [self.tableView reloadData]; }
Step 5: Loading and Saving
Our application isn't very useful if the user's bookmarks are not being saved to disk. The user would lose its bookmarks every time the application quits. Bad user experience. Let's fix this.
In our viewDidLoad: method, we send our view controller a loadBookmarks message. Let's take a look at the loadBookmarks method. We will store the user's bookmarks in the user defaults database. We first check if there already is an entry in the user defaults database with the key bookmarks
. If this is the case, we initialize our bookmarks with the contents of the stored object. It this isn't the case, we initialize our bookmarks with an empty mutable array and store that array in the user defaults database. Simple. Right?
- (void)viewDidLoad { [super viewDidLoad]; // Load Bookmarks [self loadBookmarks]; }
- (void)loadBookmarks { NSUserDefaults *ud = [NSUserDefaults standardUserDefaults]; if ([ud objectForKey:@"bookmarks"] != nil) { // Load Local Copy self.bookmarks = [NSMutableArray arrayWithArray:[ud objectForKey:@"bookmarks"]]; } else { // Initialize Bookmarks self.bookmarks = [[NSMutableArray alloc] init]; // Save Local Copy [ud setObject:self.bookmarks forKey:@"bookmarks"]; } }
Saving the bookmarks is even simpler. We store our bookmarks object in the user defaults database using the key bookmarks
. That's all there is to it.
- (void)saveBookmarks { NSUserDefaults *ud = [NSUserDefaults standardUserDefaults]; [ud setObject:self.bookmarks forKey:@"bookmarks"]; }
That was quite a bit of work. You can now build and run your application. You should be able to add bookmarks and delete bookmarks. The bookmarks are saved to disk so you won't lose any data when you quit the application.
Step 6: Synchronizing Bookmarks
To make our application really shine, we will use iCloud to keep our bookmarks synchronized across our devices. It will surprise you how easy this is. As I mentioned in the previous tutorial of this series, iCloud's Key-Value Storage works much like NSUserDefaults
, that is, it stores key-value pairs. In iCloud terminology the store is named NSUbiquitousKeyValueStore
. Let's start by saving our bookmarks not only locally, but also to iCloud.
Head over to our saveBookmarks: method and amend our method to look like the one below. We start by fetching a reference to the default store by calling defaultStore on the NSUbiquitousKeyValueStore
class. Again, this is very similar to NSUserDefaults
. If our reference to the store is not nil, we store our bookmarks object in the store with the same key we used to store our bookmarks in the user defaults database. Finally, we synchronize the store.
- (void)saveBookmarks { // Save Local Copy NSUserDefaults *ud = [NSUserDefaults standardUserDefaults]; [ud setObject:self.bookmarks forKey:@"bookmarks"]; // Save To iCloud NSUbiquitousKeyValueStore *store = [NSUbiquitousKeyValueStore defaultStore]; if (store != nil) { [store setObject:self.bookmarks forKey:@"bookmarks"]; [store synchronize]; } }
Calling synchronize on the store instance synchronizes the in-memory keys and values with the ones saved to disk. You might think that this method forces any new key-value pairs to be sent to iCloud, but this is not the case. By saving the in-memory keys to disk, iCloud is notified that there are new key-value pairs available to be synced. However, it is the operating system that ultimately decides when it is appropriate to synchronize the newly added key-value pairs with iCloud. This is important to keep in mind and this is also the reason why changes made on one device won't be visible on another device immediately.
Also, even though we called the synchronized method on the store instance, this isn't necessary in most situations. The operating system does this for you at appropriate times. Did you know that NSUserDefaults
has a synchronize method as well which works almost identically?
You might be wondering how our application knows when another device has updated the Key-Value Store in iCloud and how it can pull in those changes. This is a very good question and this is the final piece of the puzzle.
Every time the Key-Value Store has been changed, a notification is sent and our application can register as an obeserver for these notifications. It is important that you register for such notifications as early as possible in your application's life cycle.
Let's see how this works. In our viewDidLoad method we do four things. First, we get a reference to the Key-Value Store. Second, we add our view controller as an observer for any notifications with a name of NSUbiquitousKeyValueStoreDidChangeExternallyNotification
sent by the Key-Value Store. When we receive such a notification we will handle this notification in our updateKeyValuePairs: method (which we will write in a moment). Next, we send the store a message of synchronize. Finally, we load our bookmarks as we did before.
- (void)viewDidLoad { [super viewDidLoad]; NSUbiquitousKeyValueStore *store = [NSUbiquitousKeyValueStore defaultStore]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(updateKeyValuePairs:) name:NSUbiquitousKeyValueStoreDidChangeExternallyNotification object:store]; // Synchronize Store [store synchronize]; // Load Bookmarks [self loadBookmarks]; }
All that is left for us to do is implement the updateKeyValuePairs: method. Let's do that right now. The notification's userInfo
dictionary contains two important key-value pairs, (1) the reason why the notification was sent and (2) the keys in the Key-Value Store whose values were changed. We start by taking a closer look at the reason of the notification. We first make sure that a reason was specified and return if none was specified. If a reason was indeed specified, we only proceed if the reason was either a change on the server (NSUbiquitousKeyValueStoreServerChange
) or local changes were discarded because of an initial sync with the server (NSUbiquitousKeyValueStoreInitialSyncChange
). If either of these criteria is met, we pull out the array of keys that changed and we iterate over the keys to see if our bookmarks key is among them. If this is the case, we take the value associated with the key and update our data source as well as the user defaults database with this value. Finally, we reload the table view to reflect the changes.
- (void)updateKeyValuePairs:(NSNotification *)notification { NSDictionary *userInfo = [notification userInfo]; NSNumber *changeReason = [userInfo objectForKey:NSUbiquitousKeyValueStoreChangeReasonKey]; NSInteger reason = -1; // Is a Reason Specified? if (!changeReason) { return; } else { reason = [changeReason integerValue]; } // Proceed If Reason Was (1) Changes on Server or (2) Initial Sync if ((reason == NSUbiquitousKeyValueStoreServerChange) || (reason == NSUbiquitousKeyValueStoreInitialSyncChange)) { NSArray *changedKeys = [userInfo objectForKey:NSUbiquitousKeyValueStoreChangedKeysKey]; NSUbiquitousKeyValueStore *store = [NSUbiquitousKeyValueStore defaultStore]; // Search Keys for "bookmarks" Key for (NSString *key in changedKeys) { if ([key isEqualToString:@"bookmarks"]) { // Update Data Source self.bookmarks = [NSMutableArray arrayWithArray:[store objectForKey:key]]; // Save Local Copy NSUserDefaults *ud = [NSUserDefaults standardUserDefaults]; [ud setObject:self.bookmarks forKey:@"bookmarks"]; // Reload Table View [self.tableView reloadData]; } } } }
There are two points that require special attention. First, as we saw in the implementation of updateKeyValuePairs:, we receive an array containing all the keys in the Key-Value Store with values that were changed. This is an opimization to prevent iCloud from having to send a notification for every key-value pair that changed. Second, it is highly recommended that you store any data from the Key-Value Store locally. The user defaults database is well suited for this purpose, but you are free to choose whatever solution fits your needs. The reason for storing a local copy is to not rely on iCloud for data storage. Not every user will have an iCloud account or have iCloud enabled on their device.
With this final piece of code in place, our application is now iCloud enabled. Build and run your application on two devices and test it for yourself. Note that changes are not instantaneous. Changes need to sync with the iCloud servers and subsequently sent to the appropriate devices that require updating.
Even though this tuturial is lengthy, the implementation of iCloud Key-Value Storage did not require that much extra code. As I wrote in the previous tutorial, adopting Key-Value Storage is easy, straightforward, and requires very little overhead from your part.
Conclusion
Before wrapping up this tutorial, I want to reiterate the pros and cons of iCloud Key-Value Storage as they are important to keep in mind. The pros are obvious by now, it is easy to adopt, quick to implement, and it very much resembles how NSUserDefaults
works. However, it is also important to keep the cons in mind. Key-value pairs are what they are. That is, simple pairs without any form of conflict handling unless you implement your own conflict handling logic. The latter is not recommended since Key-Value Storage was not built with this in mind. Document Storage provides built-in conflict support and is therefore a much better option if your require conflict handling. Finally, don't forget that Key-Value Storage can only store 1MB worth of data with a maximum of 1024 key-value pairs. Even though we adopted Key-Value Storage in this application for illustration purposes, we cannot guarantee that there will never be a user with a list of bookmarks that surpasses the 1MB limit.
I also want to stress that the purpose of our application is to show you how to adopt Key-Value Storage and how it works behind the scenes. This is in no way an application that is ready for release as it is not bulletproof in several ways.
Next Time
In this tutorial, we learned how to set up an application for use with Apple's iCloud. We also took a closer look at iCloud Key-Value Storage. In the next tutorial, we will focus on the other type of iCloud Storage: Document Storage. This type of iCloud Storage is aimed at data heavy applications and that is exactly what we will build in the next tutorial.
Comments