title | category | date | tags | author | ||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
iCloud and Core Data |
10 |
2014-03-07 10:00:00 |
article |
|
When Steve Jobs first introduced iCloud at WWDC 2011, the promise of seamless syncing seemed too good to be true. And if you tried to implement iCloud Core Data syncing in iOS 5 or iOS 6, you know very well that it was.
Problems with syncing library-style applications continued as many developers abandoned iCloud in favor of alternatives like Simperium, TICoreDataSync, and WasabiSync.
In early 2013, after years of struggling with Apple’s opaque and buggy implementation of iCloud Core Data sync, the issues reached a breaking point when developers called out the service’s shortcomings, culminating in a pointed article by Ellis Hamburger at The Verge.
It was clear something had to change, and Apple took notice. At WWDC 2013, Nick Gillett announced that the Core Data team had spent a year focusing on fixing some of the biggest frustrations with iCloud in iOS 7, promising a vastly improved and simpler implementation for developers. “We’ve significantly reduced the amount of complex code developers have to write,” Nick said on stage at the “What’s New in Core Data and iCloud” session. With iOS 7, Apple focused on the speed, reliability, and performance of iCloud, and it shows.
Let’s take a look at what’s changed and how you can implement iCloud Core Data in your iOS 7 app.
To set up an iCloud Core Data app, you must first request access to iCloud in your application’s entitlements, which will give your app the ability to read and write to one or more ubiquity containers. You can do this easily from the new “Capabilities” screen in Xcode 5, accessible from your application’s target.
Inside a ubiquity container, the Core Data framework will store transaction logs -- records of changes made to your persistent store -- in order to sync data across devices. Core Data uses a technique called multi-master replication to sync data across multiple iOS devices and/or Macs. The persistent store file itself is stored on each device in a Core Data-managed directory called CoreDataUbiquitySupport
, inside your application sandbox. As a user changes iCloud accounts, the Core Data framework will manage multiple stores within this directory without your application having to observe the NSUbiquityIdentityDidChangeNotification
.
Each transaction log is a plist
file that keeps track of insertions, deletions, and updates to your entities. These logs are occasionally automatically coalesced by the system in a process known as baselining.
To set up your persistent store for iCloud, there are a few options you need to be aware of to pass when calling addPersistentStoreWithType:configuration:URL:options:error:
or migratePersistentStore:toURL:options:withType:error:
:
NSPersistentStoreUbiquitousContentNameKey
(NSString
)
Specifies the name of the store in iCloud (e.g.@"MyAppStore"
)NSPersistentStoreUbiquitousContentURLKey
(NSString
, optional in iOS 7)
Specifies the subdirectory path for the transaction logs (e.g.@"Logs"
)NSPersistentStoreUbiquitousPeerTokenOption
(NSString
, optional)
A per-application salt to allow multiple apps on the same device to share a Core Data store integrated with iCloud (e.g.@"d70548e8a24c11e3bbec425861b86ab6"
)NSPersistentStoreRemoveUbiquitousMetadataOption
(NSNumber
(Boolean), optional)
Used whenever you need to back up or migrate an iCloud store to strip the iCloud metadata (e.g.@YES
)NSPersistentStoreUbiquitousContainerIdentifierKey
(NSString
)
Specifies a container if your app has multiple ubiquity container identifiers in its entitlements (e.g.@"com.company.MyApp.anothercontainer"
)NSPersistentStoreRebuildFromUbiquitousContentOption
(NSNumber
(Boolean), optional)
Tells Core Data to erase the local store file and rebuild it from the iCloud data (e.g.@YES
)
The only required option for an iOS 7-only application is the content name key, which lets Core Data know where to put its logs and metadata. As of iOS 7, the string value that you pass for NSPersistentStoreUbiquitousContentNameKey
may not contain periods. If your application already uses Core Data for persistence but you haven't implemented iCloud syncing, simply adding the content name key will prepare your store for iCloud, whether or not there is an active iCloud account.
Setting up a managed object context for your application is as simple as allocating an instance of NSManagedObjectContext
and telling it about your persistent store, as well as including a merge policy. Apple recommends NSMergeByPropertyObjectTrumpMergePolicy
, which will merge conflicts, giving priority to in-memory changes over the changes on disk.
While Apple hasn’t released official sample code for iCloud Core Data in iOS 7, an Apple engineer on the Core Data team provided this basic template on the Developer Forums. We’ve edited it slightly for clarity:
#pragma mark - Notification Observers
- (void)registerForiCloudNotifications {
NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter];
[notificationCenter addObserver:self
selector:@selector(storesWillChange:)
name:NSPersistentStoreCoordinatorStoresWillChangeNotification
object:self.persistentStoreCoordinator];
[notificationCenter addObserver:self
selector:@selector(storesDidChange:)
name:NSPersistentStoreCoordinatorStoresDidChangeNotification
object:self.persistentStoreCoordinator];
[notificationCenter addObserver:self
selector:@selector(persistentStoreDidImportUbiquitousContentChanges:)
name:NSPersistentStoreDidImportUbiquitousContentChangesNotification
object:self.persistentStoreCoordinator];
}
# pragma mark - iCloud Support
/// Use these options in your call to -addPersistentStore:
- (NSDictionary *)iCloudPersistentStoreOptions {
return @{NSPersistentStoreUbiquitousContentNameKey: @"MyAppStore"};
}
- (void) persistentStoreDidImportUbiquitousContentChanges:(NSNotification *)changeNotification {
NSManagedObjectContext *context = self.managedObjectContext;
[context performBlock:^{
[context mergeChangesFromContextDidSaveNotification:changeNotification];
}];
}
- (void)storesWillChange:(NSNotification *)notification {
NSManagedObjectContext *context = self.managedObjectContext;
[context performBlockAndWait:^{
NSError *error;
if ([context hasChanges]) {
BOOL success = [context save:&error];
if (!success && error) {
// perform error handling
NSLog(@"%@",[error localizedDescription]);
}
}
[context reset];
}];
// Refresh your User Interface.
}
- (void)storesDidChange:(NSNotification *)notification {
// Refresh your User Interface.
}
In iOS 7, calling addPersistentStoreWithType:configuration:URL:options:error:
with iCloud options now returns a store almost immediately.1 It does this by first setting up an internal 'fallback' store, which is a local store that serves as a placeholder, while the iCloud store is being asynchronously built from transaction logs and ubiquitous metadata. Changes made in the fallback store will be migrated to the iCloud store when it’s added to the coordinator. Using local storage: 1
will be logged to the console when the fallback store is set up, and after the iCloud store is fully set up you should see Using local storage: 0
. This means that the store is iCloud enabled, and you’ll begin seeing imports of content from iCloud via the NSPersistentStoreDidImportUbiquitousContentChangesNotification
.
If your application is interested in when this transition between stores occurs, observe the new NSPersistentStoreCoordinatorStoresWillChangeNotification
and/or NSPersistentStoreCoordinatorStoresDidChangeNotification
(scoped to your coordinator in order to filter out internal notification noise) and inspect the NSPersistentStoreUbiquitousTransitionTypeKey
value in its userInfo
dictionary. The value will be an NSNumber boxing an enum
value of type NSPersistentStoreUbiquitousTransitionType
, which will be NSPersistentStoreUbiquitousTransitionTypeInitialImportCompleted
when this transition has occurred.
One of the worst problems with testing iCloud on iOS 5 and 6 was when heavily used accounts would encounter 'churn' and become unusable. Syncing would completely stop, and even removing all ubiquitous data wouldn’t make it work. At Lickability, we affectionately dubbed this state “f***ing the bucket.”
In iOS 7, there is a system-supported way to truly remove all ubiquitous content: +removeUbiquitousContentAndPersistentStoreAtURL:options:error:
. This method is great for testing, and may even be relevant for your application if the user gets into an inconsistent state and needs to remove all content and start over. There are a few caveats, though. First, this method is synchronous. Even while performing network operations and possibly taking a significant length of time, this method will not return until it is finished. Second, there should be absolutely no persistent store coordinators active when performing this operation. There are serious issues that can put your app into an unrecoverable state, and the official guidance is that all active persistent store coordinators should be completely deallocated beforehand.
In iOS 5, when a user switched iCloud accounts or disabled iCloud, the data in the NSPersistentStoreCoordinator
would completely vanish without letting the application know. In fact, the only way to check if an account had changed was to call URLForUbiquityContainerIdentifier
on NSFileManager
-- a method that can set up the ubiquitous content folder as a side effect, and take seconds to return. In iOS 6, this was remedied with the introduction of ubiquityIdentityToken
and its corresponding NSUbiquityIdentityDidChangeNotification
, which is posted when there is a change to the ubiquity identity. This effectively notifies the app of account changes.
In iOS 7, however, this transition is even simpler. Account changes are handled by the Core Data framework, so as long as your application responds appropriately to both NSPersistentStoreCoordinatorStoresWillChangeNotification
and NSPersistentStoreCoordinatorStoresDidChangeNotification
, it will seamlessly transition when the user’s account changes. Inspecting the NSPersistentStoreUbiquitousTransitionType
key in the userInfo
dictionary of this notification will give more detail as to the type of transition.
The framework will manage one persistent store file per account inside the application sandbox, so if the user comes back to a previous account later, his or her data will still be available as it was left. Core Data now also manages the cleanup of these files when the user’s device is running low on disk space.
Implementing a switch to enable or disable iCloud in your app is also much easier in iOS 7, although it probably isn’t necessary for most applications. Because the API now automatically creates a separate file structure when iCloud options are passed to the NSPersistentStore
upon creation, we can have the same store URL and many of the same options between both local and iCloud stores. This means that switching from an iCloud store to a local store can be done by migrating the iCloud persistent store to the same URL with the same options, plus the NSPersistentStoreRemoveUbiquitousMetadataOption
. This option will disassociate the ubiquitous metadata from the store, and is specifically designed for these kinds of migration or copying scenarios. Here's a sample:
- (void)migrateiCloudStoreToLocalStore {
// assuming you only have one store.
NSPersistentStore *store = [[_coordinator persistentStores] firstObject];
NSMutableDictionary *localStoreOptions = [[self storeOptions] mutableCopy];
[localStoreOptions setObject:@YES forKey:NSPersistentStoreRemoveUbiquitousMetadataOption];
NSPersistentStore *newStore = [_coordinator migratePersistentStore:store
toURL:[self storeURL]
options:localStoreOptions
withType:NSSQLiteStoreType error:nil];
[self reloadStore:newStore];
}
- (void)reloadStore:(NSPersistentStore *)store {
if (store) {
[_coordinator removePersistentStore:store error:nil];
}
[_coordinator addPersistentStoreWithType:NSSQLiteStoreType
configuration:nil
URL:[self storeURL]
options:[self storeOptions]
error:nil];
}
Switching from a local store back to iCloud is just as easy; simply migrate with iCloud-enabled options, and add a persistent store with same options to the coordinator.
External file references is a feature introduced for Core Data in iOS 5 that allows large binary data properties to be automatically stored outside the SQLite database on the file system. In our testing, when this occurs, iCloud does not always know how to resolve the relationship and can throw exceptions. If you plan to use iCloud syncing, consider unchecking this box in your iCloud entities:
If you are using iCloud, the contents of a store can only be migrated if the store is compatible with automatic lightweight migration. This means that Core Data must be able to infer the mapping and you cannot provide your own mapping model. Only simple changes to your model, like adding and renaming attributes, are supported. When considering whether to use Core Data syncing, be sure to think about how much your model may change in future versions of your app.
With any syncing system, conflicts between the server and the client are inevitable. Unlike iCloud Data Document Syncing APIs, iCloud Core Data integration does not specifically allow handling of conflicts between the local store and the transaction logs. That’s likely because Core Data already supports custom merge policies through subclassing of NSMergePolicy
. To handle conflicts yourself, create a subclass of NSMergePolicy
and override resolveConflicts:error:
to determine what to do in the case of a conflict. Then, in your NSManagedObjectContext
subclass, return an instance of your custom merge policy from the mergePolicy
method.
Many library-style applications display both collections of objects and detail views which display a single object. Views that are managed by NSFetchedResultsController
instances will automatically update as the Core Data ubiquity system imports changes from the network. However, you should ensure that each detail view is properly observing changes to its object and keeping itself up to date. If you don't, you risk accidentally showing stale data, or worse, saving edited values on top of newer changes from other peers.
The iCloud daemon can synchronize data across devices in one of two ways: on your local network or over the Internet. When the daemon detects that two devices, also known as peers, are on the same local area network, it will transfer the data over that presumably faster connection. If, however, the peers are on separate networks, the system will fall back to transferring transaction logs over the Internet. This is important to know, as you must test both cases heavily in development to make sure your application is functioning properly. In either scenario, syncing changes or transitioning from a fallback store to an iCloud store sometimes takes longer than expected, so if something isn't working right away, try giving it time.
One of the most helpful changes to iCloud in iOS 7 is the ability to finally use iCloud in the iOS Simulator. In previous versions, you could only test on a device, which limited how easy it was to observe the syncing process during development. Now, you can even sync data between your Mac and the simulator as two separate peers.
With the addition of the iCloud Debug Gauge in Xcode 5, you can see all files in your app's ubiquity containers and examine their file transfer statuses, such as "Current," "Excluded," and "Stored in Cloud." For more hardcore debugging, enable verbose console logging by passing -com.apple.coredata.ubiquity.logLevel 3
as a launch argument or setting it as a user default. And consider installing the iCloud Storage Debug Logging Profile on iOS and the new ubcontrol
command-line tool on OS X to provide high-quality bug reports to Apple. You can retrieve the logs that these tools generate from ~/Library/Logs/CrashReporter/MobileDevice/device-name/DiagnosticLogs
after syncing your device with iTunes.
However, iCloud Core Data is not fully supported in the simulator. When testing across devices and the simulator, it seems that iCloud Core Data on the simulator only uploads changes, and never pulls them back down. This is still a big improvement over needing separate test devices, and a nice convenience, but iCloud Core Data support on the iOS Simulator is definitely not fully baked yet.
With the clear improvements in both APIs and functionality in iOS 7, the fate of apps currently shipping iCloud Core Data on iOS 5 and iOS 6 is uncertain. Since the sync systems are so different from an API perspective (and, we’ve found out, from a functionality perspective), Apple's recommendations haven't been kind to apps that need legacy syncing. Apple explicitly recommends on the Developer Forums that you sync absolutely no data between iOS 7 and any prior version of iOS.
In fact, “at no time should your iOS 7 devices be allowed to communicate with iOS 6 peers. The iOS 6 peers will continue to exhibit bugs and issues that have been fixed in iOS 7 but in doing so will pollute the iCloud account.” The easiest way to guarantee this separation is by simply changing the NSPersistentStoreUbiquitousContentNameKey
of your store, hopefully to comply with new guidance on periods within the name. This difference guarantees that the data from older versions is siloed from the new methods of syncing, and allows developers to make a completely clean break from old implementations.
Shipping an iCloud Core Data application is still risky. You need to perform tests for everything: account switching, running out of iCloud storage, many devices, model upgrades, and device restores. Although the iCloud Debug Gauge and developer.icloud.com can help, it’s still quite a leap of faith to ship an app relying on a service that’s completely out of your control.
As Brent Simmons pointed out, shipping an app with any kind of iCloud syncing can be limiting, so be sure to understand the costs up front. Apps like Day One and 1Password have opted to let users sync their data with either iCloud or Dropbox. For many users, nothing beats the simplicity of a single account, but some power users still demand full control over their data. And for developers, maintaining disparate database syncing systems can be quite taxing during development and testing.
Once you’ve tested and shipped an iCloud Core Data application, you will likely encounter bugs in the framework. The best way to report these bugs to Apple is to file detailed Radars, which contain the following information:
- Complete steps to reproduce.
- The output to the console with iCloud debug logging on level three and the iCloud debugging profile installed.
- The full contents of the ubiquity container as a zip archive.
It’s no secret that iCloud Core Data was fundamentally broken in iOS 5 and 6, with an Apple employee acknowledging that “there were significant stability and long term reliability issues with iOS 5 / 6 when using Core Data + iCloud…The way forward is iOS 7 only, really really really really.” High-profile developers like Agile Tortoise and Realmac Software are now comfortable trusting the iCloud Core Data integration in their applications, and with enough consideration and testing, you should be too.
Special thanks to Andrew Harrison, Greg Pierce, and Paul Bruneau for their help with this article.
Footnotes
-
In previous OS versions, this method wouldn’t return until iCloud data was downloaded and merged into the persistent store. This would cause significant delays, meaning that any calls to the method would need to be dispatched to a background queue. Thankfully, this is no longer necessary. ↩