Using Core Data with iCloud seems to be one of the best kept secrets Apple have to offer. For something so relatively simple it’s a surprising brain teaser if you try to follow the scraps of documentation you find.
Let’s take a “human look” at how this works. First we’ll examine the principle, followed by code samples that actually work. It’s less complicated than it seems, so hang in there.
This approach is working fine in iOS 7.1 and 6.1. However it no longer works since iOS 8.
iCloud Core Data in Principle
[emember_protected]
Core Data and iCloud work best with the SQLIte because it’s not an atomically written store (i.e. it does’t have to be saved all at once, but can be saved in chunks). SQLite – much like MySQL – can be updated record by record using log files.
The idea is that you have a local store file, exactly like the one setup by default in the Master/Detail template. You then pass a parameter that will save the log files of each transaction to the iCloud folder. From here all devices read transactions that have not been processed and rebuild the local store files record by record.
Despite what the iOS 6 and earlier documentation may have said, since iOS 7 this is the official Apple recommended approach. In previous years you were encouraged to save the entire store file to the iCloud folder (with a .nosync suffix) but that’s no longer necessary.
To recap: use an SQLite store type and setup your app as you normally would for a “non-iCloud Core Data” project. Have your Team ID ready and an App ID that’s setup to use iCloud.
iCloud Core Data: The Code
In this example I’m starting from the Master Detail template as provided by Xcode 5.1. I’m calling it CDi – short for Core Data iCloud and perhaps an homage to Philips interactive CD project from the early nineties 😉
Let’s take a look at the custom initialiser for the NSPersistenStoreCoordinator. You’ll find it in AppDelegate.m towards the end of the file:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
- (NSPersistentStoreCoordinator *)persistentStoreCoordinator { if (_persistentStoreCoordinator != nil) { return _persistentStoreCoordinator; } NSURL *storeURL = [[self applicationDocumentsDirectory] URLByAppendingPathComponent:@"CDi.sqlite"]; NSError *error = nil; _persistentStoreCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:[self managedObjectModel]]; if (![_persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:storeURL options:nil error:&error]) { NSLog(@"Unresolved error %@, %@", error, [error userInfo]); abort(); } return _persistentStoreCoordinator; } |
This project will work out of the box. To make it play with iCloud, all we need to do is tell our Persistent Store Coordinator to look at log files in the iCloud folder (a URL) and a name for our cloud store (an NSString – optional since iOS 7).
Let’s amend the above method with the details. These need to be created as an NSDictionary and passed as options like so:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
- (NSPersistentStoreCoordinator *)persistentStoreCoordinator { if (_persistentStoreCoordinator != nil) { return _persistentStoreCoordinator; } NSURL *storeURL = [[self applicationDocumentsDirectory] URLByAppendingPathComponent:@"CDi.sqlite"]; // creating CloudStore URL and title NSURL *cloudURL = [self grabCloudPath:@"CloudLogs"]; NSString *cloudStoreTitle = @"CDi"; NSDictionary *options = @{NSPersistentStoreUbiquitousContentURLKey: cloudURL, NSPersistentStoreUbiquitousContentNameKey: cloudStoreTitle}; NSError *error = nil; _persistentStoreCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:[self managedObjectModel]]; if (![_persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:storeURL options:options error:&error]) { NSLog(@"Unresolved error %@, %@", error, [error userInfo]); abort(); } return _persistentStoreCoordinator; } |
The slightly tricky bit is how to find the URL for your ubiquitous folder. That – again – is kept under lock and key and not mentioned anywhere when this topic is discussed. Anyway… here it is, a convenient method which is called in the above code – just paste this at the end of your AppDelegate.m file:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
// Returns the URL to our Ubiquity Folder - (NSURL *)grabCloudPath:(NSString *)filename { NSFileManager *fileManager = [NSFileManager defaultManager]; NSString *teamID = @"ABCDEF1234"; // replace with your real Team ID NSString *bundleID = [[NSBundle mainBundle]bundleIdentifier]; NSString *cloudRoot = [NSString stringWithFormat:@"%@.%@", teamID, bundleID]; NSURL *cloudRootURL = [fileManager URLForUbiquityContainerIdentifier:cloudRoot]; NSString *pathToCloudFile = [[cloudRootURL path]stringByAppendingPathComponent:@"Documents"]; pathToCloudFile = [pathToCloudFile stringByAppendingPathComponent:filename]; return [NSURL fileURLWithPath:pathToCloudFile]; } |
It looks more complex than it is:
Your iCloud (or ubiquity) folder is made up of a private path on the device, your Team ID and your Bundle ID. The latter can be extracted from your project – just make sure it matches the reverse domain App ID that you’ve setup. Mine is com.versluis.buyme – I’m using it for all kinds of test jobs.
Your Team ID is a weird looking 10 digit value made up of capital letters and numbers. I’ll show you how to find it in the next article. Replace it appropriately.
The method returns a URL which we can pass in to the coordinator options.
We’re done in AppDelegate – let’s focus on MasterViewController next.
Notifications for Store Reconciliations
If Device A adds or changes a record from the store, it writes a new log entry into the CloudLog. In the background Core Data will notice this and reconcile the store file in Device B. Right now, we’d need to restart Device B for the changes to be displayed.
But that’s lame – we’d like to display new data live of course. To do this, we can subscribe to the following notification and listen to when new data has arrived in our store file. When this happens, we need to merge the changes into our managed object context so they can be displayed.
Here we setup the appropriate observer, together with a method that does the merging.
Add this to MasterViewController.m or your “listening” class:
1 2 3 4 5 6 7 8 |
// observer for store changes (perhaps in viewDidLoad) [[NSNotificationCenter defaultCenter]addObserver:self selector:@selector(contentDidChange:) name:NSPersistentStoreDidImportUbiquitousContentChangesNotification object:self.managedObjectContext.persistentStoreCoordinator]; - (void)contentDidChange:(NSNotification *)notification { // merge those changes [self.managedObjectContext mergeChangesFromContextDidSaveNotification:notification]; } |
Believe it or not – that’s all we need to do in code. And it didn’t take four scattered manuals to explain it. The Master/Detail Template uses an NSFetchedResultsController, so as soon as the managed object context has new data, it is notified and updates the table view.
Xcode Preflight Check
Before we launch this app for the first time, make sure Xcode is configured correctly:
- Check the Bundle ID and make sure this App ID is configured to use iCloud.
- In addition, in your Xcode Project under Capabilities, switch on iCloud.
- Make sure the Ubiquity Container matches your App ID / Bundle ID.
[/emember_protected]
Testing
To test this, deploy the app on two devices. Initial deployment may take some time, especially on iOS 6 depending on the store size. On iOS 7 the store is setup asynchronously and will return faster – but I can’t overemphasise this enough: Have Patience, Young Skywalker!
When both devices are up and running, add records to either device, then wait for them to show up on the other device. You can also delete and change them. It’s a lot of fun to watch!
Note that since Xcode 5 you can use the Simulator for testing too: just make sure you’re logged into the same iCloud account as your device. Simulator will take much longer to read changes, iDevices take between 10 and 30 seconds to show new results.
Working Project on GitHub
You can get the source code for this exercise on GitHub. Make sure you replace the Bundle ID (in Xcode) and Team ID (in grabCloudURL) with your own values for this to work:
Further Reading:
Note on iOS 6 and the Simulator
The iOS Simulator only understands iCloud related things as of iOS 7. Therefore, trying to run the above on the iOS 6 Simulator will end with an exception because the URL for the iCloud folder will be nil.
It will work fine on a real device though – just thought I’d mention it.