Mastodon

iCloud Complications, Part 1

As (link:/blog/icloud-sotu text:promised), I’m going to be doing a number posts on using iCloud with Core Data. I’m not sure how many there will be, I’ll keep going as long as it takes. Today I’m starting off with some things that, while not actually bugs, may catch a developer off guard. In this post I’m sticking to how iCloud is designed to work, and not getting into the questions of how and when it doesn’t work.

Some of this is covered here and there in Apple’s docs, but I haven’t seen it all spelled out. With luck this will save you some trial and error.

What have you done for my data lately?

Most of the information from Apple covers how to use iCloud in a new application. What if you want to add iCloud to an existing application that already uses Core Data?

First of all, good luck, you’ll need it. Second though, if you follow the documented approach to setting up iCloud, you’ll find that pretty much all of your data fails to sync.

With a new app, you add the NSPersistentStoreUbiquitousContentNameKey and NSPersistentStoreUbiquitousContentURLKey options when adding your persistent store, and if everything’s working right, your data just syncs.

But not if you already have some data that was created before you decided to use iCloud. Core Data’s iCloud sync is transaction based, meaning that it creates transaction logs on one device, syncs those, and replays them on other devices. In practice you get a new transaction log every time you save changes. Getting the picture yet? Your existing data is just sitting there not generating transaction logs. You have to force the issue– and migrate your existing data store to iCloud– by re-saving all of your data so that you’ll get transaction logs for it.

You can’t just run through the existing store, loading objects and re-saving them in place, because you first need to generate a transaction that creates those objects, not just one that updates them. Fortunately NSPersistentStoreCoordinator probably comes to the rescue with a method named -migratePersistentStore:toURL:options:withType:error:. That copies an existing persistent store to a new file. Its options argument takes the same arguments as you’d use when adding the store in the first place. Using this method gives you a one-line way to generate transactions for everything in the persistent store.

When adding iCloud to an existing Core Data app then, you’ll want to do something like:

  1. Look for a persistent store named according to your pre-iCloud naming convention.

  2. If you find one,

    • Load it normally, without iCloud options.
    • Migrate it to a new store file. Use a different naming convention, so that in the future it will be clear that you’ve already done the migration.
  3. If you don’t find a pre-iCloud store, look for the iCloud version and load that instead.

I say probably above because this method is going to load up your entire store in memory while migrating it. If you have a large data store, that could be a problem. If that sounds like you, you’ll need to come up with your own code to migrate to a new store. If your code works on batches of objects, saves changes after each batch, and makes careful use of -refreshObject:mergeChanges:, you can keep memory under control. This isn’t extremely complicated but it’s not a beginner’s task.

Update, April 10: I’m told that the built-in migrate call has some iCloud-related issues on iOS 6.1, as described in a discussion at Apple’s devforums site. Most of my iCloud work has been on Mac OS X, which is probably why I haven’t run into these issues. One suggested workaround is Drew McCormack’s MCPersistentStoreMigrator. I haven’t tried it yet, but Drew knows his iCloud+Core Data stuff.

Awake from what?

It’s common to use -[NSManagedObject awakeFromInsert] in custom managed object classes to initialize default data. Usually developers regard the method as a one-time object initialization, so this makes sense. This perspective is (apparently) supported by the documentation, which says that this method is:

Invoked automatically by the Core Data framework when the receiver is first inserted into a managed object context.

Except… what if the same instance is inserted into a different managed object context? Normally this wouldn’t be an issue. In fact, -[NSManagedObjectContext insertObject:] throws an exception if you try to insert an object that has already been inserted into another context.

But guess what? If you create a managed object, add it to a managed object context, and it gets synced to another device, that instance gets inserted into a new managed object context on the remote device. It’s a different object on a different device, representing the same data in a different managed object context. So your custom awakeFromInsert runs again.

One approach to dealing with this is to move your custom initialization code out of awakeFromInsert into a separate custom method that you call directly. Problem solved? Maybe, but maybe not. This isn’t the only method that iCloud calls for you when syncing objects. If you have any custom setter methods, iCloud calls those too. Custom validation methods? Yeah, you get the picture. It’s all there at iCloud import time.

I found that to be kind of stunning when I first encountered it. I had expected iCloud to just copy the incoming data to my persistent store– not to load up my custom code and run it on my behalf. I already validated those values when I created the instance in the first place, why validate it again? A bizarre but functional workaround is to subclass NSManagedObjectContext but not actually change it in any way. The subclass doesn’t add anything except the ability to tell whether a managed object is arriving from iCloud. Because while iCloud might be a little too helpful in getting your custom managed object subclasses, it has no way of knowing about your custom managed object context subclass.

The class interface is empty aside from the name:

@interface MyManagedObjectContext : NSManagedObjectContext

@end

The implementation is similarly bare:

@implementation MyManagedObjectContext

@end

Use this class every time you create a managed object context:

NSManagedObjectContext *context = [[MyManagedObjectContext alloc]
    initWithConcurrencyType:NSPrivateQueueConcurrencyType];

You can then do something like this in your custom code:

- (void)awakeFromInsert
{
    if ([[self managedObjectContext] isKindOfClass:[MyManagedObjectContext class]]) {
        // Created locally
    } else {
        // Importing from iCloud
    }
}

Next up: Duplicate detection

In the next episode of iCloud Complications I’ll talk about how your data store can unexpectedly explode into a mess of duplicate (or triplicate, or worse) objects, and how you can stop it from happening.