The Mental Blog

Software with Intellect

6 notes

Under the Sheets with iCloud and Core Data: The Basics

There can be no doubt that iCloud is an important part of Apple’s future. It’s now less than a year old, but it has been a mixed start. The service itself seems up to the enormous data traffic it has to push, but some aspects of client-side integration are still very rough around the edges.

You don’t have to look far in Apple’s developer forums to know that Core Data syncing is one of those rough spots. An appeal on Twitter, and extensive Googling, led me to just one shipping app that currently offers Core Data syncing over iCloud: Time Butler. I know of no other shipping app that is using it, and certainly no apps from well-known, established developers.

(All eyes are currently on Bare Bones, who have vowed to offer iCloud syncing in their shoebox app Yojimbo before MobileMe syncing is turned off in a few months. Time will tell if they achieve that goal.)

Mental Case

I’ve spent the last 3-4 months integrating Core Data syncing over iCloud into Mental Case for Mac. It is not in the wild yet, but we have started beta testing with a limited audience.

To say it has been a challenge would be an understatement — it has probably been one of the hardest tasks I have ever undertaken for a Mac or iOS app. And so, in an effort to move the state of Core Data/iCloud syncing forward, I have decided to pen a number of posts, in the hope that others will be able to cross the same hurdles faster.

Apple has very little in the way of documentation on Core Data syncing. They do explain the basics of setting up your app, loading your Core Data store, and handling merge notifications, but they don’t go into any detail about how syncing works, and certain scenarios that you will need to deal with in a shipping app. That’s what I plan to cover in these posts.

(Before getting started, it is worth pointing out that Daniel Pasco of Black Pixel delivered an excellent presentation at NSConference 2012, covering many of the issues that I’ll be discussing. You can purchase a video of his talk from the NSConference site.)

Some Restrictions

Let’s start with a few gotchas, some of which wasted days of my time, and are completely undocumented.

  1. Core Data syncing via iCloud does not work in apps using Garbage Collection. This is not documented, it simply does not work. If you have a GC app, time to move to ARC.
  2. iCloud syncing only works for apps sold through the Mac App Store. If you plan to sell your app outside the Mac App Store, you will have to come up with creative ways to enable iCloud syncing. (PDFpen managed to get iCloud syncing enabled for off-store copies by selling a small cloud sync enabler app via the Mac App Store.)
  3. You can only use automatic migration for your iCloud data. If you need to do more extreme migrations, you will need to setup some sort of store versioning, and copy your data to a whole new store.
  4. You cannot use ordered relationships.

If you can cope with these restrictions, you are ready to face iCloud.

Setting Up

In the next few sections, I’ll quickly skim through what Apple covers in its own documentation. This is the bare minimum that you need to get iCloud Core Data syncing working. Unfortunately, it is far from enough to get your app to the production stage.

The first step is to login at the Mac Dev Center and click the link to go to the Developer Certificate Utility. (I will discuss iCloud from the perspective of Mac app development, but much of the information will apply equally to an iOS app.) Select the App IDs, and configure your app to use iCloud. Having done that, you will have to regenerate any provisioning profiles for your app, and install them in Xcode.

Now in Xcode, you should select your app’s target, and in the Summary tab, enable entitlements. You can add one or more iCloud containers here; these correspond to folders in iCloud. The first one should have the same name as the bundle identifier of your app (e.g. com.mentalfaculty.mentalcase.mac). Additional containers can be added with other names. You should use the reverse-DNS naming convention for these containers. Extra containers could be used, for example, to share data between multiple apps, such as when you want to support cross-platform syncing of Macs and iOS devices.

iCloud Containers

In your code, you can request a file URL for your iCloud container using the new NSFileManager method URLForUbiquityContainerIdentifier:. If you only have one container, you can pass in nil. If you have more than one, you need to pass in an identifier.

The identifier is comprised of your Team Identifier Prefix, followed by the container id you entered in the entitlements section (e.g. “<Team ID>.com.mentalfaculty.mentalcase”). So where do you find the Team Identifier Prefix? If you go to the Member Center of the Apple Developer site, and you click the organization link at the top, you should see an entry called ‘Company/Organization ID’. That is the Team Identifier Prefix you need to use.

You should call URLForUbiquityContainerIdentifier: early on in an app’s launch. If it returns nil, it means iCloud is not enabled on the current device. If it returns a file URL, iCloud is active, and the URL gives you a path to the iCloud container, which is a folder in ~/Library/Mobile Documents. Anything you put in there will get synced to other devices using the same container.

Setting Up the Core Data Stack

Having tested whether iCloud is turned on, you are ready to setup your Core Data stack. This is almost the same as when you aren’t using iCloud, with some small differences. If iCloud is on, i.e. if URLForUbiquityContainerIdentifier: returned a URL, you should add extra options for your persistent store.

    NSMutableDictionary *options = [NSMutableDictionary dictionaryWithObjectsAndKeys:
        [NSNumber numberWithBool:YES], NSMigratePersistentStoresAutomaticallyOption,
        [NSNumber numberWithBool:YES], NSInferMappingModelAutomaticallyOption, 
        nil];

    // Add cloud options    
    if ( storeURL ) {
        [options addEntriesFromDictionary:
            [NSDictionary dictionaryWithObjectsAndKeys:
                @"com.mentalfaculty.mentalcase.mainstore.1", NSPersistentStoreUbiquitousContentNameKey,
                storeURL, NSPersistentStoreUbiquitousContentURLKey, 
                nil]];

The value for NSPersistentStoreUbiquitousContentNameKey should be a unique name for the store. It gets mapped to a subfolder in iCloud. The container URL you retrieved above should be passed in with the key NSPersistentStoreUbiquitousContentURLKey.

With the options all ready, you can add your persistent store to the persistent store coordinator.

    dispatch_queue_t backgroundQueue = dispatch_get_global_queue(0,0);
    dispatch_async(backgroundQueue, ^{
        NSError *error;
        [persistentStoreCoordinator lock];
        diskStore = [persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:localURL options:options error:&error];
        [persistentStoreCoordinator unlock];

The localURL is simply the file URL that you are using for the store. This will usually be in the user’s Library folder somewhere (e.g. Application Support folder).

When you invoke the addPersistentStore… method, it is important to do it on a background thread. With iCloud enabled, this method can block execution for some time, while iCloud retrieves any files it might need from the servers.

With a persistent store coordinator setup, you can now create an NSManagedObjectContext. You probably don’t usually pay much attention to the merge policy it is using, but when integrating with iCloud, you should change the mergePolicy property to something other than the default. A reasonable choice is NSMergeByPropertyObjectTrumpMergePolicy.

Observing Merge Notifications

The last part of a basic iCloud/Core Data setup is to listen for notifications that iCloud has merged changes from another device. Each merge corresponds to a save performed on another device, so it is possible that several notifications fire off in short succession, as iCloud imports the changes one save at a time.

To be notified of merges, simply observe the NSPersistentStoreDidImportUbiquitousContentChangesNotification notification (Is that the longest name in Cocoa?), which is sent by your persistent store coordinator. When you receive the notification, you need to merge the changes into your managed object context in much the same way that you would merge save changes when using multiple contexts.

-(void)persistentStoreCoordinatorDidMergeCloudChanges:(NSNotification *)notification
{
    [managedObjectContext performBlock:^{        
        [managedObjectContext mergeChangesFromContextDidSaveNotification:notification]; 

You will also need to consider whether managed objects that are retained in your app could be invalidated by a merge, and perhaps need to be re-fetched when the notification fires.

Devil in the Details

That’s basically all that Apple will tell you about Core Data syncing with iCloud, and also all you will tend to find in tutorials on the subject. You could mistakenly be led to believe that integrating iCloud in your Core Data app will be a walk in the park. Unfortunately, at this juncture, nothing could be farther from the truth.

In the coming posts, we will consider the devil in the details, including…

  1. How iCloud Core Data syncing works under the hood.
  2. How you get existing data into iCloud from multiple devices.
  3. Migrating your app from an existing, legacy sync scheme.
  4. What happens when a device has iCloud temporarily disabled.
  5. How you can handle conflicts and validation failures.

Stay tuned.

Filed under coredata icloud mac ios software

  1. itsthejb reblogged this from mentalfaculty
  2. mentalfaculty posted this