Under the Sheets with iCloud and Core Data: Working Code
Having covered Core Data syncing via iCloud from a high level, in this post I want to introduce a simple test app, which will be extended over the coming weeks. The app should eventually contain most of the code snippets you will need to get your own app up and running with iCloud.
Installing the Test App
The test app is available on github. You can download the source code as a zip archive, but it is probably best to use git to clone the project to your Mac, so that you can pull future changes as we go.
Once you have the source code, there are a few modifications you will need to make if you want to sync via iCloud. First, go into the AppDelegate.m file, and enter your Team ID at the top.
static NSString * const TeamIdentifier = @"XXXXXXXXXX";
Simply replace the placeholder string with your Team ID.
Next, you will need to go to the Developer Certificate Utility from the Mac Dev Center, and create a provisioning profile, as described in the first post. It is probably easiest to create the profile for a wildcard app. To do this, just use an asterisk (*) as the app bundle identifier. Once you download the provisioning profile, install it on your Mac by double clicking the file in Finder. Also add it to the Provisioning Profiles section under the Devices tab in the Xcode Organizer. Lastly, go into the iCloudCoreDataTester Target’s Build Settings and set the Code Signing Identity to use the new profile.
Introduction to iCloudCoreDataTester
The app is structurally very simple: all the interesting code is in the AppDelegate.m file. The Core Data entity model is based on parts of the model from my app, Mental Case, but is otherwise nonsense. Best not to try to understand what any of it means; just note the types of relationships involved, the deletion rules, and other aspects that may be of interest. The objective in designing the entity model was to mimic the sort of complex relationships that you might find in a production app.
The UI of the app is very straightforward. The list on the left shows a list of Note objects, which you can add and remove using the buttons underneath. If you select a note, a child object appears in the list on the right. This ‘schedule’ object can be switched using the Change Schedule button, which deletes the existing instance, and inserts a new one.

The model includes various hidden entities, which don’t appear in the UI, and are mainly there to setup different types of relationships, to thoroughly test iCloud.
At this point, the app does not handle stoppages. It is more a developer test apparatus than a user-friendly, end-user app. There are buttons to manually setup and tear down the Core Data stack, delete the local store, delete the cloud container, save the managed object context, and turn on iCloud syncing. (An entry in NSUserDefaults is used to persist whether iCloud syncing is enabled or not.)
The Core Data Stack
The AppDelegate begins with a number of simple methods for retrieving URLs for the local store and cloud container. Local data is located in the user’s Application Support folder
-(NSURL *)localStoreURL
{
return [self.applicationFilesDirectory URLByAppendingPathComponent:@"iCloudCoreDataTester.storedata"];
}
and the cloud container is setup in a subpath (MainStore) of the main container.
-(NSURL *)cloudStoreURL
{
NSString *bundleId = [[NSBundle mainBundle] bundleIdentifier];
NSString *ubiquityId = [NSString stringWithFormat:@"%@.%@", TeamIdentifier, bundleId];
NSURL *ubiquitousURL = [[NSFileManager defaultManager] URLForUbiquityContainerIdentifier:ubiquityId];
NSURL *storeURL = [ubiquitousURL URLByAppendingPathComponent:@"MainStore"];
return storeURL;
}
Each of the buttons in the UI is connected to a basic action. You will probably want similar methods in your own app, so that it is trivial to setup and teardown the Core Data stack, remove the cloud container, or perform other fundamental tasks at will.
-(IBAction)tearDownCoreDataStack:(id)sender
{
if ( !stackIsSetup ) return;
NSError *error;
if ( ![self.managedObjectContext save:&error] ) {
[NSApp presentError:error];
}
stackIsSetup = NO;
[self.managedObjectContext reset];
self.managedObjectContext = nil;
self.managedObjectModel = nil;
self.persistentStoreCoordinator = nil;
}
-(IBAction)setupCoreDataStack:(id)sender
{
if ( stackIsSetup ) return;
stackIsSetup = YES;
[self willChangeValueForKey:@"managedObjectContext"];
[self didChangeValueForKey:@"managedObjectContext"];
}
The iCloudCoreDataTester app uses Cocoa Bindings in the user interface, so setting up the Core Data stack just involves firing KVO notifications, which cause the table views to reload, and thereby lazy creation of the Core Data stack objects (eg NSPersistentStoreCoordinator, NSManagedObjectContext, etc).
Creation of the Core Data stack objects is fairly standard, and follows the prescription given in the first post. The persistent store coordinator adds the local store with ubiquity options if iCloud syncing is enabled.
// Use cloud storage if iCloud is enabled, and the user default is set to YES.
NSURL *storeURL = self.cloudStoreURL;
BOOL usingCloudStorage = [[NSUserDefaults standardUserDefaults] boolForKey:UsingCloudStorageDefault];
usingCloudStorage &= storeURL != nil;
// Basic options
NSMutableDictionary *options = [NSMutableDictionary dictionaryWithObjectsAndKeys:
(id)kCFBooleanTrue, NSMigratePersistentStoresAutomaticallyOption,
(id)kCFBooleanTrue, NSInferMappingModelAutomaticallyOption,
nil];
// iCloud options
if ( usingCloudStorage ) {
[options addEntriesFromDictionary:[NSDictionary dictionaryWithObjectsAndKeys:
MCCloudMainStoreFileName, NSPersistentStoreUbiquitousContentNameKey,
storeURL, NSPersistentStoreUbiquitousContentURLKey,
nil]];
}
Merging Changes
When the persistent store coordinator is created, the AppDelegate registers to receive notifications of iCloud merges.
// Register as observer for iCloud merge notifications
[[NSNotificationCenter defaultCenter] removeObserver:self];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(persistentStoreCoordinatorDidMergeCloudChanges:)
name:NSPersistentStoreDidImportUbiquitousContentChangesNotification object:coordinator];
When the notification is sent, it is passed directly to the mergeChangesFromContextDidSaveNotificaton: method of the NSManagedObjectContext.
-(void)persistentStoreCoordinatorDidMergeCloudChanges:(NSNotification *)notification
{
[self.managedObjectContext performBlock:^{
[self.managedObjectContext mergeChangesFromContextDidSaveNotification:notification];
NSError *error;
if ( ![self.managedObjectContext save:&error] ) {
[NSApp presentError:error];
}
}];
}
File Coordination
When Apple developed iCloud, they came across issues that don’t affect an isolated desktop app. One of those issues was that, with iCloud in the mix, files often need to be accessed from multiple processes at once. Apple needed to develop a robust locking and notification mechanism for file access, and that became the file coordination and presentation classes.
Whenever you need to perform any action at all on a file (or folder) in the iCloud container, you should use an NSFileCoordinator, to make sure you play nicely with the iCloud processes that are monitoring and modifying the same files. In iCloudCoreDataTester, an NSFileCoordinator is used when removing the iCloud container.
-(IBAction)removeCloudFiles:(id)sender
{
[self tearDownCoreDataStack:self];
[[NSUserDefaults standardUserDefaults] setBool:NO forKey:UsingCloudStorageDefault];
NSFileCoordinator* coordinator = [[NSFileCoordinator alloc] initWithFilePresenter:nil];
NSURL *storeURL = self.cloudStoreURL;
if ( !storeURL ) return;
[coordinator coordinateWritingItemAtURL:storeURL options:NSFileCoordinatorWritingForDeleting error:NULL byAccessor:^(NSURL *newURL) {
[[NSFileManager defaultManager] removeItemAtURL:newURL error:NULL];
}];
}
The method coordinateWritingItemAtURL:options:error:byAccessor: is responsible for retreiving a lock on the container’s folder URL, in order to perform our file operation, in this case a deletion (NSFileCoordinatorWritingForDeleting). In doing so, it informs any NSFilePresenter objects that are monitoring the container what is about to happen. In this way, iCloud will notice that the data has been deleted.
The actual removal is carried out using the default NSFileManager in a block passed as last argument to the method. You may think that this happens asynchrously, as is usually the case with block arguments, but the NSFileCoordinator methods are synchronous — the block has been executed, and the container deleted, when the method returns.
Note also that you should operate on the URL passed into the block, rather than the original storeURL. This is a good habit to get into, because with some file operations, it is possible the file’s URL may have been modified by previous operations.
Migration
Migrating an existing local store to iCloud is the task of the migrateStoreToCloud method. It begins by moving the local store to a new URL.
-(void)migrateStoreToCloud
{
NSError *error;
NSURL *storeURL = self.localStoreURL;
NSURL *oldStoreURL = [[self applicationFilesDirectory] URLByAppendingPathComponent:@"OldStore"];
NSFileManager *fileManager = [NSFileManager defaultManager];
// If there is no local store, no need to migrate
if ( ![fileManager fileExistsAtPath:storeURL.path] ) return;
// Remove any existing old store file left over from a previous migration
[fileManager removeItemAtURL:oldStoreURL error:NULL];
// Move existing local store aside
if ( ![fileManager moveItemAtURL:storeURL toURL:oldStoreURL error:&error] ) {
[[NSApplication sharedApplication] presentError:error];
return;
}
Store options are then generated for the existing non-ubiquitous store, and the new ubiquitious store.
// Options for new cloud store
NSDictionary *localOnlyOptions = [NSDictionary dictionaryWithObjectsAndKeys:
(id)kCFBooleanTrue, NSMigratePersistentStoresAutomaticallyOption,
(id)kCFBooleanTrue, NSInferMappingModelAutomaticallyOption,
nil];
NSDictionary *cloudOptions = [NSDictionary dictionaryWithObjectsAndKeys:
(id)kCFBooleanTrue, NSMigratePersistentStoresAutomaticallyOption,
(id)kCFBooleanTrue, NSInferMappingModelAutomaticallyOption,
MCCloudMainStoreFileName, NSPersistentStoreUbiquitousContentNameKey,
self.cloudStoreURL, NSPersistentStoreUbiquitousContentURLKey,
nil];
An NSPersistentStoreCoordinator is created, and the non-ubiquitious store added.
NSPersistentStoreCoordinator *coordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:self.managedObjectModel];
id oldStore = [coordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:oldStoreURL options:localOnlyOptions error:&error];
if ( !oldStore ) {
// Move back the old store and present the error
[fileManager moveItemAtURL:oldStoreURL toURL:storeURL error:NULL];
[[NSApplication sharedApplication] presentError:error];
return;
}
The migratePersistentStore:toURL:options:withType:error: method is used to carry out the migration to the ubiquitous store, which is located at the original store URL.
// Migrate existing (old) store to new store
if ( ![coordinator migratePersistentStore:oldStore toURL:storeURL options:cloudOptions withType:NSSQLiteStoreType error:&error] ) {
[fileManager removeItemAtURL:storeURL error:NULL];
[fileManager moveItemAtURL:oldStoreURL toURL:storeURL error:NULL];
[[NSApplication sharedApplication] presentError:error];
return;
}
If all goes well, the old non-ubiquitious store is deleted.
else {
[[NSFileManager defaultManager] removeItemAtURL:oldStoreURL error:NULL];
}
}
Care should be taken to ensure no data is lost in this process. It might even be better not to delete the old store outright, but to simply leave it in place or move it to the trash, so that if a problem arises, the user has some recourse to retrieve the old data.
What’s Next…
iCloudCoreDataTester is already useful to test various syncing scenarios. You should install it on two Macs, or virtual machines, and try to get it syncing.
At this point, it is not very robust. In particular, it has no facilities for handling stoppages, so if you remove the iCloud container on one machine, you will probably find the app crashes on the other machine after iCloud has synced the change.
In the next post, I’ll introduce a so-called sentinel file, which will be used to notify the app of unexpected changes to the iCloud container, and allow it to respond graciously, rather than just crashing.