Because Core Data keeps track of changes via transaction logs, it’s impossible to just “turn on” iCloud in an existing application and expect all the data to get pushed into the cloud. A few other steps are necessary.
The first question when adding iCloud to an existing iOS application is whether the migration is necessary. There are two key criteria for answering this question.
Both of these questions can be answered easily if we do a simple filename change. For example, if our application has always used a SQLite file named PPRecipes.sqlite, then when we want to add iCloud integration to our application, we should start using a filename of PPRecipes-iCloud.sqlite. A simple “does this file exist?” check tells us whether we need to migrate our existing data.
If it’s not possible or reasonable to rename the file, the fallback option is to store a flag in the NSUserDefaults to let us know whether the migration has occurred. This option is second best for a couple of reasons.
Assuming we’re going to use a file rename strategy to determine whether a migration is required, the first step is to look for the “old” filename to determine whether a migration is required. As part of this change to handle the migration, we’re going to refactor the NSPersistentStoreCoordinator initialization code somewhat to make it more maintainable with these additions.
| dispatch_queue_t queue; |
| queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); |
| dispatch_async(queue, ^{ |
| NSMutableDictionary *options = [[NSMutableDictionary alloc] init]; |
| [options setValue:[NSNumber numberWithBool:YES] |
| forKey:NSMigratePersistentStoresAutomaticallyOption]; |
| [options setValue:[NSNumber numberWithBool:YES] |
| forKey:NSInferMappingModelAutomaticallyOption]; |
| NSFileManager *fileManager = [NSFileManager defaultManager]; |
| NSURL *docURL = nil; |
| docURL = [[fileManager URLsForDirectory:NSDocumentDirectory |
| inDomains:NSUserDomainMask] lastObject]; |
| NSURL *storeURL = nil; |
| |
| NSError *error = nil; |
| NSPersistentStoreCoordinator *coordinator = nil; |
| coordinator = [[self managedObjectContext] persistentStoreCoordinator]; |
| NSPersistentStore *store = nil; |
| |
| NSURL *cloudURL = [fileManager URLForUbiquityContainerIdentifier:nil]; |
We start the changes at the top of the asynchronous dispatch queue. Notice that we’re setting the “universal” options only for the NSPersistentStore at this point. This step allows us to reuse the dictionary no matter what path we end up taking. We’re also obtaining our reference to the NSPersistentStoreCoordinator here, because that will be used through the rest of the block. Finally, we request the cloudURL from the NSFileManager so that we can start to determine how to add the NSPersistentStore to the NSPersistentStoreCoordinator.
Now we’re ready to make our first decision: is iCloud available or not?
| if (!cloudURL) { |
| storeURL = [docURL URLByAppendingPathComponent:@"PPRecipes.sqlite"]; |
| store = [coordinator addPersistentStoreWithType:NSSQLiteStoreType |
| configuration:nil |
| URL:storeURL |
| options:options |
| error:&error]; |
| if (!store) { |
| ALog(@"Error adding persistent store to coordinator %@\n%@", |
| [error localizedDescription], [error userInfo]); |
| //Present a user facing error |
| return; |
| } |
| if ([self initBlock]) { |
| dispatch_sync(dispatch_get_main_queue(), ^{ |
| [self initBlock](); |
| }); |
| } |
| return; |
| } |
Now that we’ve added the migration code for iCloud, it’s actually the shorter path when iCloud isn’t enabled. Therefore, we’re going to respond to that decision first. If iCloud isn’t available, we look for the file named PPRecipes.sqlite and add it to the persistent store. If the file doesn’t exist, Core Data will create it. This is the traditional logic path.
Once the NSPersistentStore is added to the NSPersistentStoreCoordinator, we check to make sure it was successful and then notify our UIApplicationDelegate that the stack initialization is complete and return. It should be noted that it’s possible for the user to turn iCloud back off and be fully robust. We should check to see whether that situation occurred. If it did, we must migrate back off of iCloud. That decision branch is left as an exercise for the reader.
| storeURL = [docURL URLByAppendingPathComponent:@"PPRecipes-iCloud.sqlite"]; |
| NSURL *oldURL = nil; |
| oldURL = [docURL URLByAppendingPathComponent:@"PPRecipes.sqlite"]; |
| if ([fileManager fileExistsAtPath:[oldURL path]]) { |
| store = [coordinator addPersistentStoreWithType:NSSQLiteStoreType |
| configuration:nil |
| URL:oldURL |
| options:options |
| error:&error]; |
| if (!store) { |
| ALog(@"Error adding OLD persistent store to coordinator %@\n%@", |
| [error localizedDescription], [error userInfo]); |
| //Present a user facing error |
| return; |
| } |
| } |
| cloudURL = [cloudURL URLByAppendingPathComponent:@"PPRecipes"]; |
| [options setValue:[[NSBundle mainBundle] bundleIdentifier] |
| forKey:NSPersistentStoreUbiquitousContentNameKey]; |
| [options setValue:cloudURL |
| forKey:NSPersistentStoreUbiquitousContentURLKey]; |
| store = [coordinator migratePersistentStore:store |
| toURL:storeURL |
| options:options |
| withType:NSSQLiteStoreType |
| error:&error]; |
| if (!store) { |
| ALog(@"Error adding OLD persistent store to coordinator %@\n%@", |
| [error localizedDescription], [error userInfo]); |
| //Present a user facing error |
| return; |
| } |
| |
| ZAssert([fileManager removeItemAtURL:oldURL error:&error], |
| @"Failed to remove old persistent store at %@\n%@\n%@", |
| oldURL, [error localizedDescription], [error userInfo]); |
| |
| if ([self initBlock]) { |
| dispatch_sync(dispatch_get_main_queue(), ^{ |
| [self initBlock](); |
| }); |
| } |
| }); |
Now we come to the more complicated decision. iCloud is enabled, but we don’t know whether a migration is needed. First, we go ahead and complete the storeURL with the “new” filename, PPRecipes-iCloud.sqlite. Next, we construct the “old” file URL for PPRecipes.sqlite. If the “old” URL exists (via the NSFileManager), then we need to perform a migration.
We add the “old” file to the NSPersistentStoreCoordinator and obtain a reference to the NSPersistentStore. Once we confirm that it was loaded successfully, we can proceed with the migration.
Since we want the “new” store to be connected to iCloud, we now need to add in the options for iCloud configuration to our options dictionary. These are the options we discussed in Configuring iCloud. Once the options dictionary has been updated, we can kick off the migration via a call to ‑migratePersistentStore: toURL: options: withType: error:. This call does several things:
Creates a new SQLite file at the location specified by storeURL
Copies all the data from the “old” file to the “new” file
Registers the “new” file with iCloud per the options specified in the dictionary
Removes the “old” store from the NSPersistentStoreCoordinator
Adds the “new” SQLite file to the NSPersistentStoreCoordinator
It’s a lot of work for one line of code, and it should be noted that this line of code can take some time. Therefore, depending on our user experience, we may want to broadcast a notification before the work begins so our user interface updates and lets the user know what’s going on.
Assuming the migration was successful, we now need to delete the old SQLite file from disk so we don’t accidentally repeat these steps on the next launch.
Once the migration and the deletion are complete, we’re finally ready to notify the UIApplicationDelegate that the Core Data stack is ready for use.