Introduction

This blog post provides a step-by-step guide for getting started with iCloud and Core Data in iOS 6. The combination of Core Data and iCloud in iOS 6 represents an exciting opportunity for developers. Anyone who remembers trying to sync the calendar on a Palm Pilot with Outlook has a good idea of how far we've come. Before iOS 6, some apps, like OmniFocus, set up their own sync servers. Others relied on hard-wired syncing through iTunes. Some sync over a WiFi connection. Until iCloud and iOS 6, the infrastructure and technical requirements for syncing across devices were really beyond the capabilities of most small and medium-sized development teams.

It is important to understand the limitations of iCloud syncing with Core Data. First, remember that we are only talking about syncing between different devices owned by the same user. This is not about two friends sharing data. For that, an independent syncing server is still required. Second, the technology is new. Only experience will teach us how well the system scales to what will surely be robust demand.

Even so, allowing a single user to view the same data across all his or her devices, especially when it is data generated by your app, provides a better experience and is going to be a big deal for iOS developers!

This blog post presents a quick way to see iCloud working with Core Data. Though Core Data is a deep and demanding area, it is possible to get a simple app up and syncing in a couple of hours. Here I lead you through that happy path. Think of it as nothing more than a proof of concept and have fun! The results won't be fancy, but you'll have a good starting point to build App Store-ready apps that offer users a compelling reason to choose your product.

Step 1: Start with a working Core Data project

Core Data is a powerful framework for managing objects and persistence on Mac OS X and iOS. If you come from the .Net world, think Entity Framework (EF) blended in with a little bit of LINQ. On the Java side, its best analogue would be Hibernate. Also, like EF and Hibernate, the learning curve for Core Data is rather steep. Each presents dozens of configuration options, options whose utility does not become apparent before hours of twiddling have elapsed. Take the choice of a persistent store for example. Users choose between binary store, xml store, or SQLite database. Most, I image, opt for the SQLite database. But for now, we just want to get iCloud to work with Core Data, so it is best to start with a project that is already built that way.

For my example, I'm choosing the CoreDataBooks sample app from Apple’s developer site. There are two reasons for this: 1) I don’t want to get bogged down with the configuration of a Core Data project, but more importantly, 2) The CoreDataBooks project uses SQLite for its persistent store. Using SQLite is Apple's recommended approach, and for good reason. If you use XML, the entire XML document is uploaded and downloaded with each sync to iCloud. It is hard to see why anyone would choose XML for anything other than development purposes. The same is true for binary stores. With SQLite, on the other hand, only the transactions in the change log are sent to iCloud for processing.

Plus, CoreDataBooks is a ridiculously simple application. It simply stores a list of book titles, authors, and copyright dates in a single entity. The app presents the data in a UITableView. User's can perform standard CRUD operations on the list.

Step 2: Configure the Core Data Project for iOS 6

The first step is to make sure your project is configured for iOS 6. There are two places to make this change. In the project settings page, select the Build Target, then choose the Summary tab. Set the Deployment Target to 6.0. This sets the lowest version of iOS that your app will support.

Setting the Deployment Target

Then, make sure the base SDK for the project is set to 6.0. Click on the build settings tab, then enter "SDK" in the filter box, and look for the "Base SDK" build option. If you don’t see a 6.0 option, you are probably not running Xcode 4.5 or better. Fix that and come back.

Configure the Base SDK

Now you should be able to build and run the project in the simulator under iOS 6. If you got this far, then you're ready to hop onto Apple's developer site to get your app provisioned for iCloud.

Step 3: Set up your test devices

To do any kind of meaningful testing, you need to have more than one device registered in your name. Here you can see I have my wife's iPhone and my iPhone and iPad registered. If you can, testing with three is even more illuminating than testing with just two. Importantly, make sure that each of these devices is running iOS6. Also, remember that this tutorial is all about the happy path for Core Data and iCloud. So make sure each of the devices has iCloud activated and it currently using the same iCloud account.

Step 4: Provision your App for iCloud

Log into the Member Center at developer.apple.com. Make sure that each of your test devices are registered.

Configure Your devices

Then you need to create a unique App Id for your app. Wild card App ID's won't work with iCloud. So click on the App ID’s option and choose the "New App ID" button. After the new App ID is created, you still need to configure it, so click on the Configure button. Then check off the "Enable for iCloud" option.

Enable Your Profile for iCloud

Now it is time to create a mobile provision for your app. This is what enables your application to run on actual devices rather than just the simulator. In other words, it joins your App ID to a specific set of devices. Here you can see that I've selected all of my devices for the profile.

Set up your Provisioning Profile

Next, switch over to Xcode's Organizer, select Provisioning Profile on the left hand side and then click the Refresh button at the bottom. When you see the new profile pop up, it's a good time to connect each of the devices you plan to use for testing, and drag the new profile over to them.

Once your new provisioning profile shows up, go back to your project's Build Settings page, and filter the list for "Code Signing." You want to use the developer identity listed just below your new provisioning profile.

Set the Code Signing Identity

Step 5: Enable Entitlements on your Xcode Project

Now everything is set up except iCloud! You need to request the capabilities and acquire the permissions necessary to use iCloud storage. Go back to Summary page of Build settings. Scroll on down to the bottom, and the check "Enable iCloud" checkbox. Then add a ubiquity container for your app. If you use Core Data with iCloud, you need to create a ubiquity container, since it uses iCloud Document storage (as opposed to iCloud key-value storage). You can have more than one ubiquity container, but for this application, we only need one. Also, the identifier string must the same as your bundle identifier.

Enable Entitlements

When you make this selection, you'll see a new file appear in your project's file tree. The CoreDataBooks.entitlements file is just a property list that reflects the entries you made above.

Entitlements pList

As a sanity check, you should be able to build and run your app now. It won't use iCloud because we haven't put anything into the application's ubiquity container, but if somewhere along the line you made a configuration error, now is the time to fix it, before we get into the code.

Step 6: Take a breath and consider the Big Picture

Here is an overview of what we need to do make the original code from CoreDataBooks work for iCloud. First determine whether or not the user has an iCloud account. If so, if this is the first launch of the application, we need to create the persistence store for Core Data in a special folder in the iCloud ubiquity container. Don't let that phrase "ubiquity container" scare you off. There's a bit of marketing-speak in that name. From the app's perspective, it is just a folder. But think of it like a DropBox or SugarSync folder. Everything that changes in that folder will be shipped off to Apple's iCloud servers. Once our SQLite's transaction logs start getting pumped into that folder, they will be synced between all our devices. iOS 6 will then take care up updating the SQLite database. Finally, we need to set up notification observers so we'll know when our persistent store has been updated so we can refresh the UI.

Apple recommends that most all the code regarding iCloud configuration take place very early in the application's lifecycle. That means in one or more of the various lifecycle methods in the App Delegate. For this very simple example, all the code you will see is called from application:didFinishLaunchingWithOptions:.

Step 7: Determine if the user has an iCloud account

One of the most important new methods in iOS 6, relative to iCloud, is ubiquityIdentityToken, an instance method of the NSFileManager. Here is how you would call it.

self.fileManager <span class="sy0">=</span> <span class="br0">[</span><a href="http://developer.apple.com/documentation/Cocoa/Reference/Foundation/Classes/NSFileManager_Class/"><span class="kw5">NSFileManager</span></a> defaultManager<span class="br0">]</span>;
<span class="kw4">id</span> currentToken <span class="sy0">=</span> <span class="br0">[</span>self.fileManager ubiquityIdentityToken<span class="br0">]</span>;
isSignedIntoICloud<span class="sy0">=</span> <span class="br0">(</span>currentToken<span class="sy0">!=</span><span class="kw2">nil</span><span class="br0">)</span>;

You'll learn a lot from this method. If the user has not enabled iCloud on his or her phone, the currentToken value will be nil. If it's not nil, you'll want to save it someplace like NSUserDefaults, because if the current token changes, you'll need to adjust your persistent store accordingly (and that scenario is beyond the happy path scope of this post). If you have configured iCloud on your test devices, that token will not be nil and it will be the same on each device. The code below shows how to save the token.

<span class="sy0">-</span><span class="br0">(</span><span class="kw4">void</span><span class="br0">)</span> persistICloudToken<span class="br0">{</span>
 <span class="kw1">if</span> <span class="br0">(</span>isFirstLaunchOfApplication <span class="sy0">&&</span> isSignedIntoICloud<span class="br0">)</span><span class="br0">{</span>
 <span class="kw4">id</span> currentiCloudToken <span class="sy0">=</span> <span class="br0">[</span><span class="br0">[</span><a href="http://developer.apple.com/documentation/Cocoa/Reference/Foundation/Classes/NSFileManager_Class/"><span class="kw5">NSFileManager</span></a> defaultManager<span class="br0">]</span> ubiquityIdentityToken<span class="br0">]</span>;
<span class="kw1">if</span> <span class="br0">(</span>currentiCloudToken<span class="br0">)</span> <span class="br0">{</span>
 <a href="http://developer.apple.com/documentation/Cocoa/Reference/Foundation/Classes/NSData_Class/"><span class="kw5">NSData</span></a> <span class="sy0">*</span>newTokenData <span class="sy0">=</span><span class="br0">[</span><a href="http://developer.apple.com/documentation/Cocoa/Reference/Foundation/Classes/NSKeyedArchiver_Class/"><span class="kw5">NSKeyedArchiver</span></a> archivedDataWithRootObject<span class="sy0">:</span> currentiCloudToken<span class="br0">]</span>;
 <span class="br0">[</span><span class="br0">[</span><a href="http://developer.apple.com/documentation/Cocoa/Reference/Foundation/Classes/NSUserDefaults_Class/"><span class="kw5">NSUserDefaults</span></a> standardUserDefaults<span class="br0">]</span>
 setObject<span class="sy0">:</span> newTokenData
 forKey<span class="sy0">:</span> <span class="co3">@</span><span class="st0">"com.mbroski.CoreDataBooks.UbiquityIdentityToken"</span><span class="br0">]</span>;
 <span class="br0">}</span> <span class="kw1">else</span> <span class="br0">{</span>
 <span class="br0">[</span><span class="br0">[</span><a href="http://developer.apple.com/documentation/Cocoa/Reference/Foundation/Classes/NSUserDefaults_Class/"><span class="kw5">NSUserDefaults</span></a> standardUserDefaults<span class="br0">]</span>
 removeObjectForKey<span class="sy0">:</span> <span class="co3">@</span><span class="st0">"com.mbroski.CoreDataBooks.UbiquityIdentityToken"</span><span class="br0">]</span>;
 <span class="br0">}</span>
 <span class="br0">}</span>
<span class="br0">}</span>

Step 8: Get the User's permission to use iCloud storage

Assuming that the user has an iCloud account, you need to get their permission to enable syncing across devices. After all, the user might not want to enable syncing. For this app, I simply display a UIAlertView to get the permission. If permission is granted, then I proceed with configuration for iCloud. If denied, then configure the persistence store for Core Data exactly as your app currently does.

<span class="sy0">-</span><span class="br0">(</span><span class="kw4">void</span><span class="br0">)</span> requestPermissionToUseICloud<span class="br0">{</span>
 <span class="kw1">if</span> <span class="br0">(</span>isFirstLaunchOfApplication <span class="sy0">&&</span> isSignedIntoICloud<span class="br0">)</span><span class="br0">{</span>
 UIAlertView <span class="sy0">*</span>alert <span class="sy0">=</span> <span class="br0">[</span><span class="br0">[</span>UIAlertView alloc<span class="br0">]</span> initWithTitle<span class="sy0">:</span> <span class="co3">@</span><span class="st0">"Choose Storage Option"</span>
 message<span class="sy0">:</span> <span class="co3">@</span><span class="st0">"Should documents be stored in iCloud and available on all your devices?"</span>
 delegate<span class="sy0">:</span> self
 cancelButtonTitle<span class="sy0">:</span> <span class="co3">@</span><span class="st0">"Local Only"</span>
 otherButtonTitles<span class="sy0">:</span> <span class="co3">@</span><span class="st0">"iCloud"</span>, <span class="kw2">nil</span><span class="br0">]</span>;
 
 <span class="br0">[</span>alert show<span class="br0">]</span>;
 
 <span class="br0">}</span>
<span class="br0">}</span>

Step 9: Configure the Data Directory for the Persistent Store Coordinator

Back in Step 6, I compared the iCloud Ubiquity Container to a DropBox folder. Anything that goes into the folder is going to be synced with Apple's iCloud servers. But there is one very important exception to that rule! If a directory in the Ubiquity Container has the extension *.nosync, then any file therein will NOT be synced. And it is into that folder we're going to put the SQLite database. The transaction logs will go into the ubiquity container, but not into the *.nosync folder, so they will be part of the syncing process. To create a directory in the ubiquity container, we first have to find the path to that container. Here's how I do it.

<span class="sy0">-</span><span class="br0">(</span><a href="http://developer.apple.com/documentation/Cocoa/Reference/Foundation/Classes/NSURL_Class/"><span class="kw5">NSURL</span></a> <span class="sy0">*</span><span class="br0">)</span> createICloudDataDirectory<span class="br0">{</span>
 <a href="http://developer.apple.com/documentation/Cocoa/Reference/Foundation/Classes/NSURL_Class/"><span class="kw5">NSURL</span></a> <span class="sy0">*</span>storeURL <span class="sy0">=</span> <span class="br0">[</span>self.fileManager URLForUbiquityContainerIdentifier<span class="sy0">:</span><span class="co3">@</span><span class="st0">"XXXXXXXX.com.mbroski.CoreDataBooks"</span><span class="br0">]</span>;
 <a href="http://developer.apple.com/documentation/Cocoa/Reference/Foundation/Classes/NSURL_Class/"><span class="kw5">NSURL</span></a> <span class="sy0">*</span>dataFolder <span class="sy0">=</span> <span class="br0">[</span>storeURL URLByAppendingPathComponent<span class="sy0">:</span><span class="co3">@</span><span class="st0">"Data.nosync"</span><span class="br0">]</span>;
 <span class="co2">//that nosync file extension is important!</span>
 <a href="http://developer.apple.com/documentation/Cocoa/Reference/Foundation/Classes/NSURL_Class/"><span class="kw5">NSURL</span></a> <span class="sy0">*</span>dataFile <span class="sy0">=</span> <span class="br0">[</span>dataFolder URLByAppendingPathComponent<span class="sy0">:</span><span class="co3">@</span><span class="st0">"CoreDataBooks.sqlite"</span><span class="br0">]</span>;
 <span class="kw4">BOOL</span> isDirectory <span class="sy0">=</span> <span class="kw2">YES</span>;
 <a href="http://developer.apple.com/documentation/Cocoa/Reference/Foundation/Classes/NSError_Class/"><span class="kw5">NSError</span></a> <span class="sy0">*</span>error;
 <span class="kw1">if</span><span class="br0">(</span><span class="sy0">!</span><span class="br0">[</span>self.fileManager fileExistsAtPath<span class="sy0">:</span><span class="br0">[</span>dataFolder path<span class="br0">]</span> isDirectory<span class="sy0">:&</span>isDirectory<span class="br0">]</span><span class="br0">)</span>
 <span class="kw1">if</span><span class="br0">(</span><span class="sy0">!</span><span class="br0">[</span>self.fileManager createDirectoryAtURL<span class="sy0">:</span>dataFolder withIntermediateDirectories<span class="sy0">:</span><span class="kw2">YES</span> attributes<span class="sy0">:</span><span class="kw2">nil</span> error<span class="sy0">:&</span>error<span class="br0">]</span><span class="br0">)</span>
 NSLog<span class="br0">(</span><span class="co3">@</span><span class="st0">"Create Directory Error %@, %@"</span>, error, <span class="br0">[</span>error userInfo<span class="br0">]</span><span class="br0">)</span>;
 NSLog<span class="br0">(</span><span class="co3">@</span><span class="st0">"dataFile=%@"</span>,<span class="br0">[</span>dataFile path<span class="br0">]</span><span class="br0">)</span>;
 <span class="kw1">return</span> dataFile;
 
<span class="br0">}</span>
<span class="sy0">/</span>objc>
 

This method creates the iCloud data directory and returns the&nbsp;<a href="http://developer.apple.com/documentation/Cocoa/Reference/Foundation/Classes/NSURL_Class/"><span class="kw5">NSURL</span></a>

reference to the SQLite store that will go within it, CoreDataBooks.sqlite. The URLForUbiquityContainerIdentifier method takes a string argument this is the full bundle name, including company identifier, for my app. From that path, I'm creating a Data.nosync folder. It is into that folder we're going to put the SQLite database.

Step 10: Configure the Core Data Persistent Store Coordinator

Now that the directory for the SQLite database is created, we need to configure Core Data's Persistent Store Coordinator to use it. Also, Core Data needs to be told that there is a Ubiquity Container where all the SQLite transaction logs need to go. Here is that code:

<span class="sy0">-</span><span class="br0">(</span><span class="kw4">void</span><span class="br0">)</span> addPersistentStore<span class="sy0">:</span><span class="br0">(</span><a href="http://developer.apple.com/documentation/Cocoa/Reference/Foundation/Classes/NSURL_Class/"><span class="kw5">NSURL</span></a><span class="sy0">*</span><span class="br0">)</span>dataFileURL<span class="br0">{</span>
 <a href="http://developer.apple.com/documentation/Cocoa/Reference/Foundation/Classes/NSError_Class/"><span class="kw5">NSError</span></a> <span class="sy0">*</span>error;
 <a href="http://developer.apple.com/documentation/Cocoa/Reference/Foundation/Classes/NSURL_Class/"><span class="kw5">NSURL</span></a> <span class="sy0">*</span>storeURL <span class="sy0">=</span> <span class="br0">[</span>self.fileManager URLForUbiquityContainerIdentifier<span class="sy0">:</span><span class="co3">@</span><span class="st0">"XXXXXXXX.com.mbroski.CoreDataBooks"</span><span class="br0">]</span>;
 NSLog<span class="br0">(</span><span class="co3">@</span><span class="st0">"[storeURL path]=%@"</span>,<span class="br0">[</span>storeURL path<span class="br0">]</span><span class="br0">)</span>;
 <a href="http://developer.apple.com/documentation/Cocoa/Reference/Foundation/Classes/NSDictionary_Class/"><span class="kw5">NSDictionary</span></a> <span class="sy0">*</span>options <span class="sy0">=</span> @<span class="br0">{</span>NSPersistentStoreUbiquitousContentNameKey <span class="sy0">:</span> <span class="co3">@</span><span class="st0">"iCloudData"</span>,
 NSPersistentStoreUbiquitousContentURLKey <span class="sy0">:</span> storeURL<span class="br0">}</span>;
 <span class="kw4">id</span> result<span class="sy0">=</span><span class="br0">[</span>_persistentStoreCoordinator addPersistentStoreWithType <span class="sy0">:</span> NSSQLiteStoreType
 configuration <span class="sy0">:</span> <span class="kw2">nil</span>
 URL <span class="sy0">:</span> dataFileURL
 options <span class="sy0">:</span> options
 error <span class="sy0">:</span> <span class="sy0">&</span>error<span class="br0">]</span>;
 <span class="kw1">if</span> <span class="br0">(</span>result<span class="sy0">==</span><span class="kw2">nil</span><span class="br0">)</span><span class="br0">{</span>
 NSLog<span class="br0">(</span><span class="co3">@</span><span class="st0">"Unresolved error %@, %@"</span>, error, <span class="br0">[</span>error userInfo<span class="br0">]</span><span class="br0">)</span>;
 <span class="br0">}</span>
<span class="br0">}</span>

Pay careful attention to the options variable. We are going to use this dictionary to configure Core Data to use iCloud. First we add the value for the NSPersistentStoreUbiquitousContentNameKey. This name should be unique for each ubiquity container in use. Since this app only uses one ubiquity container, the name really isn't important. But the second key, NSPersistentStoreUbiquitousContentURLKey, is vital. Its value points to the ubiquity container where Core Data will store the transaction logs. Here we are putting it right at the root level of the ubiquity container, though for neatness sake, you could add another subfolder to the container if you wish.

Then, we tell the Persistent Store Coordinator to create this new persistent store. We tell it to use SQLite. We tell it to use the NSURL for the SQLite file we generated in Step 9, and most importantly, we pass in the optionsfor Core Data.

Step 10: Register for Notifications when Core Data is Updated by iCloud

If you build and run the app now, your UI will have no idea when the data in the persistent store has been updated. That will be disappointing after all this configuration. The only way you'll see changes from other devices to force termination of the app. Much better to request a notification whenever the underlying data store has been updated by iCloud. In the CoreDataBooks project, I added the following to viewDidLoad of the root view controller.

<span class="br0">[</span><span class="br0">[</span><a href="http://developer.apple.com/documentation/Cocoa/Reference/Foundation/Classes/NSNotificationCenter_Class/"><span class="kw5">NSNotificationCenter</span></a> defaultCenter<span class="br0">]</span>
 addObserver<span class="sy0">:</span> self
 selector<span class="sy0">:</span> <span class="kw1">@selector</span> <span class="br0">(</span>iCloudChangesImported<span class="sy0">:</span><span class="br0">)</span>
 name<span class="sy0">:</span> NSPersistentStoreDidImportUbiquitousContentChangesNotification
 object<span class="sy0">:</span> <span class="kw2">nil</span><span class="br0">]</span>;

Now, whenever content changes in the persistent store, the app will be ready to handle it. Here is the code for iCloudChangesImported:

<span class="sy0">-</span><span class="br0">(</span><span class="kw4">void</span><span class="br0">)</span> iCloudChangesImported<span class="sy0">:</span><span class="br0">(</span><a href="http://developer.apple.com/documentation/Cocoa/Reference/Foundation/Classes/NSNotification_Class/"><span class="kw5">NSNotification</span></a> <span class="sy0">*</span><span class="br0">)</span>notification <span class="br0">{</span>
 NSLog<span class="br0">(</span><span class="co3">@</span><span class="st0">"iCloud changes have been imported=%@"</span>,<span class="co3">@</span><span class="st0">"YES"</span><span class="br0">)</span>;
 <a href="http://developer.apple.com/documentation/Cocoa/Reference/Foundation/Classes/NSError_Class/"><span class="kw5">NSError</span></a> <span class="sy0">*</span>error;
 <span class="br0">[</span><span class="br0">[</span>self fetchedResultsController<span class="br0">]</span> performFetch<span class="sy0">:&</span>error<span class="br0">]</span>;
 dispatch_async<span class="br0">(</span>dispatch_get_main_queue<span class="br0">(</span><span class="br0">)</span>, <span class="sy0">^</span><span class="br0">(</span><span class="kw4">void</span><span class="br0">)</span> <span class="br0">{</span>
 <span class="br0">[</span>self.tableView reloadData<span class="br0">]</span>;
 <span class="br0">}</span><span class="br0">)</span>;
<span class="br0">}</span>

First, I tell the fetchedResultsController to bring back a fresh set of data. Then the tableview is instructed to reload data. And that's all there is to it.

Step 11: Install and run on your test devices

One after another, connect each of your devices and build and run the app. Once the app is installed, however, they do not need to remain tethered to your Mac. It is useful to run the profiler network monitor to view the traffic over the device from your app. Add and change entries, and you should see their mirror images pop up on each test device. All this without writing any service calls, without composing complex synchronization algorithms, or changing the project's original entity structure in any way. Plus, even in airplane mode, the app should operate identically. Whenever the network connection is restored, all pending transactions will be uploaded to iCloud. Pretty neat!

Next Steps: Think about the real world

In order to get the app running in as few steps as possible, we made a lot of heroic assumptions. As always, app demons lurk in the details.

  • We assumed the user was already signed into iCloud when the app was launched. It is quite possible that the user would sign out of iCloud while the app is running.
  • If you need to pre-populate your data, you will need special seeding techniques. You can migrate contents using the migratePersistentStore:toURL:options:withType:error: method.
  • We assumed away latency issues. Apple recommends using background threads whenever persistent stores are manipulated.
  • Even though iCloud sync is designed for single users, there can be concurrency issues. It is possible that the same user might be editing the same record on two different devices at the same time.

All that said, Core Data and iCloud in iOS 6 provides a major step forward for developers creating content across devices that is not only compelling, but consistent.