I will attempt to answer my own question, partly to organise my thoughts and partly to reply to @DuncanGroenewald.
1) Does this approach have any drawbacks or border cases that might not be obvious at first glance?
Yes. The local and iCloud account stores are managed by Core Data and can be removed at any time.
In practice, I don't think the local account store will be ever removed as it cannot be recreated from iCloud.
Regarding iCloud account stores, I can see two scenarios in which they might be removed: a) to free space after the user turned iCloud off or b) because the user requested it by selecting Settings > iCloud > Delete All.
If the user requested it, then you might argue that data migration is not a concern.
If if was to free space, then yes, it's a problem. However, the same problem exists in any other method as your app is not woken up when iCloud account stores are removed.
2) Are NSPersistentStoreCoordinatorStoresWillChangeNotification and NSPersistentStoreCoordinatorStoresDidChangeNotification sufficient to detect all the possible on to off and off to on iCloud transitions?
Yes. It requires you to always create the persistent store with NSPersistentStoreUbiquitousContentNameKey
, no matter if iCloud is on or off. Like this:
[self.managedObjectContext.persistentStoreCoordinator
addPersistentStoreWithType:NSSQLiteStoreType
configuration:nil
URL:storeURL
options:@{ NSPersistentStoreUbiquitousContentNameKey : @"someName" }
error:&error];
In fact, only listening to NSPersistentStoreCoordinatorStoresDidChangeNotification
is enough (as shown below). This will be called when the store is added at startup or changed during execution.
3) Would you do the user prompt and merging between NSPersistentStoreCoordinatorStoresWillChangeNotification and NSPersistentStoreCoordinatorStoresDidChangeNotification, or gather all the information in those and wait until the store is changed? I ask because these notifications appear to be sent in background, and blocking them to perform a potentially long operation might not be what Core Data expects.
This is how I would do it in NSPersistentStoreCoordinatorStoresDidChangeNotification
.
Since this notification is sent both at startup and when the store changes during execution, we can use it to save the current store url and ubiquity identity token (if any).
Then we check if we are in a on/off transition scenario and migrate data accordingly.
For brevity's sake, I'm not including any UI code, user prompts or error handling. You should ask (or at the very least inform) the user before doing any migration.
- (void)storesDidChange:(NSNotification *)notification
{
NSDictionary *userInfo = notification.userInfo;
NSPersistentStoreUbiquitousTransitionType transitionType = [[userInfo objectForKey:NSPersistentStoreUbiquitousTransitionTypeKey] integerValue];
NSPersistentStore *persistentStore = [userInfo[NSAddedPersistentStoresKey] firstObject];
id<NSCoding> ubiquityIdentityToken = [NSFileManager defaultManager].ubiquityIdentityToken;
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
if (transitionType != NSPersistentStoreUbiquitousTransitionTypeInitialImportCompleted) { // We only care of cases if the store was added or removed
NSData *previousArchivedUbiquityIdentityToken = [defaults objectForKey:HPDefaultsUbiquityIdentityTokenKey];
if (previousArchivedUbiquityIdentityToken) { // Was using ubiquity store
if (!ubiquityIdentityToken) { // Changed to local account
NSString *urlString = [defaults objectForKey:HPDefaultsPersistentStoreURLKey];
NSURL *previousPersistentStoreURL = [NSURL URLWithString:urlString];
[self importPersistentStoreAtURL:previousPersistentStoreURL
isLocal:NO
intoPersistentStore:persistentStore];
}
} else { // Was using local account
if (ubiquityIdentityToken) { // Changed to ubiquity store
NSString *urlString = [defaults objectForKey:HPDefaultsPersistentStoreURLKey];
NSURL *previousPersistentStoreURL = [NSURL URLWithString:urlString];
[self importPersistentStoreAtURL:previousPersistentStoreURL
isLocal:YES
intoPersistentStore:persistentStore];
}
}
}
if (ubiquityIdentityToken) {
NSData *archivedUbiquityIdentityToken = [NSKeyedArchiver archivedDataWithRootObject:ubiquityIdentityToken];
[defaults setObject:archivedUbiquityIdentityToken forKey:HPModelManagerUbiquityIdentityTokenKey];
} else {
[defaults removeObjectForKey:HPModelManagerUbiquityIdentityTokenKey];
}
NSString *urlString = persistentStore.URL.absoluteString;
[defaults setObject:urlString forKey:HPDefaultsPersistentStoreURLKey];
dispatch_async(dispatch_get_main_queue(), ^{
// Update UI
});
}
Then:
- (void)importPersistentStoreAtURL:(NSURL*)importPersistentStoreURL
isLocal:(BOOL)isLocal
intoPersistentStore:(NSPersistentStore*)persistentStore
{
if (!isLocal) {
// Create a copy because we can't add an ubiquity store as a local store without removing the ubiquitous metadata first,
// and we don't want to modify the original ubiquity store.
importPersistentStoreURL = [self copyPersistentStoreAtURL:importPersistentStoreURL];
}
if (!importPersistentStoreURL) return;
// You might want to use a different concurrency type, depending on how you handle migration and the concurrency type of your current context
NSManagedObjectContext *importContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSConfinementConcurrencyType];
importContext.persistentStoreCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:self.managedObjectModel];
NSPersistentStore *importStore = [importContext.persistentStoreCoordinator
addPersistentStoreWithType:NSSQLiteStoreType
configuration:nil
URL:importPersistentStoreURL
options:@{NSPersistentStoreRemoveUbiquitousMetadataOption : @(YES)}
error:nil];
[self importContext:importContext intoContext:_managedObjectContext];
if (!isLocal) {
[[NSFileManager defaultManager] removeItemAtURL:importPersistentStoreURL error:nil];
}
}
The data migration is performed in importContext:intoContext
. This logic will depend of your model and duplicate and conflict policies.
I can't tell if this might have unwanted side effects. Obviously it might take quite a while depending on the size and data of the persistent store. If I find any problems I will edit the answer.