If you’ve been using Core Data for a while, you’ll feel right at home creating a Core Data stack; otherwise, this code will look similar to the stack we discussed in Chapter 2, Under the Hood of Core Data. The code to add iCloud to the Core Data stack is short, straightforward, and easy to add. This is good and bad. It’s good, in that it takes a small amount of effort to add iCloud to your Core Data--based application, but it’s bad because there aren’t very many options to configure, and it’s a one-size-fits-all design. If your data model is complex or if you have elements that you don’t want to sync, then lack of configurability will cause some interesting solutions. For example, if you have nonsyncable data, then you may need to split your data into more than one persistent store. Another situation that can feel limiting is if you have a very high churn rate in your data structures. iCloud prefers to have an opportunity to process the data, and a high rate of content creation or change can cause it to get backed up. In that situation, it may be necessary to “roll up” your data changes to decrease the number of entities being created or the frequency of saves. Reviewing your application’s activities in Instruments (as discussed in Chapter 5, Performance Tuning) will help you to determine whether you have strayed off the golden path.
To integrate iCloud with our Core Data stack, we insert some additional options to the NSPersistentStore when we add the NSPersistentStore to the NSPersistentStoreCoordinator.
| NSMutableDictionary *options = [[NSMutableDictionary alloc] init]; |
| [options setValue:[NSNumber numberWithBool:YES] |
| forKey:NSMigratePersistentStoresAutomaticallyOption]; |
| [options setValue:[NSNumber numberWithBool:YES] |
| forKey:NSInferMappingModelAutomaticallyOption]; |
| NSFileManager *fileManager = [NSFileManager defaultManager]; |
| NSURL *cloudURL = [fileManager URLForUbiquityContainerIdentifier:nil]; |
| if (cloudURL) { |
| DLog(@"iCloud enabled: %@", cloudURL); |
| cloudURL = [cloudURL URLByAppendingPathComponent:@"PPRecipes"]; |
| [options setValue:[[NSBundle mainBundle] bundleIdentifier] |
| forKey:NSPersistentStoreUbiquitousContentNameKey]; |
| [options setValue:cloudURL |
| forKey:NSPersistentStoreUbiquitousContentURLKey]; |
| } else { |
| DLog(@"iCloud is not enabled"); |
| } |
The first part of this code should be familiar. We create an NSMutableDictionary and add the options both to infer a mapping model and to attempt a migration automatically. From here, though, we’re in new territory. ‑URLForUbiquityContainerIdentifier: is a new addition to the NSFileManager that came with iCloud. This call requests a URL used to store information that iCloud is going to use to sync the NSPersistentStore. If iCloud is disabled on this device (or Mac OS X computer), this call will return nil. Once we have the URL and have established that it’s not nil, we know iCloud is available, and we can begin to configure it.
The URL we receive points to a file path; it looks something like this:
| file://localhost/private/var/mobile/Library/ |
| Mobile%20Documents/K7T84T27W4~com~pragprog~PPRecipes/ |
Notice this file path is outside our application sandbox. Even so, we have some control over what goes into this directory. For example, if we use a document-based application, we could append the name of the document onto this path so that each document is kept separate. In our current example, however, we’re going to create a single subdirectory for our Core Data stack. This will help us in the future if we decide to make changes or sync additional items. Once the URL is defined, we need to add it to our options dictionary with the key NSPersistentStoreUbiquitousContentURLKey.
In addition to the URL for the storage location, we must tell iCloud what data it should be syncing with. If we have a single application shared between iPhone and iPad, as in our current example, we can use the bundle identifier as a unique key to define what data is to be shared across the devices. However, if we’re also sharing data with a desktop application, the bundle identifier may not be appropriate. The data identifier is stored in the options dictionary with the key NSPersistentStoreUbiquitousContentNameKey.
Once again, the addition of those two keys is the bare minimum required to enable iCloud for an iOS application. With that information, the operating system creates a directory for the content, downloads any content that exists in the cloud, and starts syncing that data for us. However, that initial download (or for that matter subsequent syncing) can take an indeterminate amount of time. If there’s nothing currently in the store, the creation of the directory structure will be virtually instantaneous. However, if there’s data to download, it could take some time, depending on the speed of the network connection and the amount of data. As before, we must change how we add the NSPersistentStore to the NSPersistentStoreCoordinator.
Prior to iOS 6.0 and Mac OS X Lion, we could add the NSPersistentStore to the NSPersistentStoreCoordinator directly on the main thread. While this was rarely recommended, it was extremely common. With the addition of iCloud, it’s really no longer an option. The process of configuring iCloud happens when we add the NSPersistentStore to the NSPersistentStoreCoordinator, and it happens before the call returns. If iCloud needs to download data and that download takes several seconds, our application will be unresponsive while the download occurs, and our application could be potentially killed from the operating system watchdog for taking too long to start up.
Currently, the best solution to this problem is to add the NSPersistentStore to the NSPersistentStoreCoordinator on a background thread. We can use dispatch queues and blocks to make this relatively painless.
| 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 *cloudURL = [fileManager URLForUbiquityContainerIdentifier:nil]; |
| if (cloudURL) { |
| DLog(@"iCloud enabled: %@", cloudURL); |
| cloudURL = [cloudURL URLByAppendingPathComponent:@"PPRecipes"]; |
| [options setValue:[[NSBundle mainBundle] bundleIdentifier] |
| forKey:NSPersistentStoreUbiquitousContentNameKey]; |
| [options setValue:cloudURL |
| forKey:NSPersistentStoreUbiquitousContentURLKey]; |
| } else { |
| DLog(@"iCloud is not enabled"); |
| } |
| NSURL *sURL = nil; |
| sURL = [[fileManager URLsForDirectory:NSDocumentDirectory |
| inDomains:NSUserDomainMask] lastObject]; |
| sURL = [sURL URLByAppendingPathComponent:@"PPRecipes-iCloud.sqlite"]; |
| NSError *error = nil; |
| NSPersistentStoreCoordinator *coordinator = nil; |
| coordinator = [[self managedObjectContext] persistentStoreCoordinator]; |
| NSPersistentStore *store = nil; |
| |
| |
| |
| |
| |
| store = [coordinator addPersistentStoreWithType:NSSQLiteStoreType |
| configuration:nil |
| URL:sURL |
| options:options |
| error:&error]; |
| if (!store) { |
| ALog(@"Error adding persistent store to coordinator %@\n%@", |
| [error localizedDescription], [error userInfo]); |
| //Present a user facing error |
| } |
| if ([self initBlock]) { |
| dispatch_sync(dispatch_get_main_queue(), ^{ |
| [self initBlock](); |
| }); |
| } |
| }); |
In this code block, we define the path for our SQLite file and gain a reference to the NSPersistentStoreCoordinator. From there, we add the NSPersistentStore to the NSPersistentStoreCoordinator. Assuming that is successful, we push another block back to the main queue and inform the application that the Core Data stack has been completed and is ready for use.
The reason we grab a fresh reference to the NSPersistentStoreCoordinator is one of safety. If we were to use the reference from the outer method, we’d be incrementing the retain count of the NSPersistentStoreCoordinator and potentially causing an unnecessary reference count issue. Since we can easily obtain a fresh reference to it, there’s no reason to use the reference from the outer method.
Likewise, once we’ve completed the construction of the NSPersistentStoreCoordinator, we want to be on the main thread (aka the UI Thread) when we call ‑contextInitialized so that the rest of the AppDelegate doesn’t need to dance with threads. Keeping all of the thread jumping in one place makes it easier to maintain.