Using iCloud
6.5 Migrating an Existing Application
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.
Detecting a Migration
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.
• Is there any existing data to migrate?
• Has the migration already been performed?
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 is 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.
• As we will demonstrate in a moment, the file needs to be moved anyway.
• NSUserDefaults classes tend to be a bit unreliable, especially during testing.
Assuming we are 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 are going to refactor the NSPersistentStoreCoordinator initialization code some-what to make it more maintainable with these additions.
iCloud/PPRecipes/PPRAppDelegate.m 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];
We start the changes at the top of the asynchronous dispatch queue. Notice that we are 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 are 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 are ready to make our first decision: is iCloud available or not?
iCloud/PPRecipes/PPRAppDelegate.m
ALog(@"Error adding persistent store to coordinator %@\n%@", [error localizedDescription], [error userInfo]);
Now that we’ve added the migration code for iCloud, it is actually the shorter path when iCloud is not enabled. Therefore, we are going to respond to that decision first. If iCloud is not available, we look for the file named PPRecipes.sqlite and add it to the persistent store. If the file does not exist, Core Data will create it. This is the traditional logic path.
Chapter 6. Using iCloud
•
114Once 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 is 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.
iCloud/PPRecipes/PPRAppDelegate.m
ALog(@"Error adding OLD persistent store to coordinator %@\n%@", [error localizedDescription], [error userInfo]);
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]);
dispatch_sync(dispatch_get_main_queue(), ^{
[self contextInitialized];
});
});
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, on page 106. 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 do not accidentally repeat these steps on the next launch.
Once the migration and the deletion are complete, we are finally ready to notify the UIApplicationDelegate that the Core Data stack is ready for use.