amitsbajaj
amitsbajaj

Reputation: 1304

Migrate iCloud data to Local store and stopping iCloud from still responding

I am so close to completing the implementation of iCloud and Core Data in my app. This is an iOS 7 only app.

I am providing the user with an option within the app to say Use iCloud On or Off in the form of a UISwitch. The app starts out and asks the user if they want to use iCloud or not and this can then later be changed within the app.

I have the data migrating from the local store (if it exists) to iCloud. The one aspect I'm struggling with is migrating the data from the iCloud Store to the Local store.

Scenario: - User A has an iPhone with my app and some data - User A downloads the app onto the iPad, selects to use iCloud but then at a later point, decides they do not want synching on the iPad. So they turn off the iPad iCloud synching within the app.

At this point, my understanding is that a user would expect the data to still be there, but from behind the scenes, it's now "LOCAL" instead of "iCloud" based data which means, entries from the other device won't synchronise over to this device and vice versa

So I am trying to achieve that.

Issue

I can get the data to migrate from the iCloud to the local store, but the issue is that because iCloud is still enabled on the device, the next time the app is run, I can see that iCloud is disabled within the app but the console still shows Using Local Storage 1 and then 0 and any data from the iPhone synchronises across to the iPad, even though the Use iCloud UISwitch is very clearly off.

Here's the main code doing the migration:

- (void)migrateiCloudStoreToLocalStore {

    NSLog(@"Migrate iCloudToLocalStore");
    NSPersistentStore *store = self.persistentStoreCoordinator.persistentStores.lastObject;

    //NSURL *storeURL = [[self applicationDocumentsDirectory] URLByAppendingPathComponent:@"Envylope.sqlite"];
    NSURL *storeURL = [self.persistentStoreCoordinator.persistentStores.lastObject URL];
    NSLog(@"Current Store URL (before iCloud to Local migration): %@", [storeURL description]);

    NSDictionary *localStoreOptions = nil;
    localStoreOptions = @{ NSPersistentStoreRemoveUbiquitousMetadataOption : @YES,
                           NSMigratePersistentStoresAutomaticallyOption : @YES,
                           NSInferMappingModelAutomaticallyOption : @YES};

    NSPersistentStore *newStore = [self.persistentStoreCoordinator migratePersistentStore:store
                                                                                     toURL:storeURL
                                                                                   options:localStoreOptions
                                                                                  withType:NSSQLiteStoreType error:nil];

    [self reloadStore:newStore];
}

- (void)reloadStore:(NSPersistentStore *)store {
    NSLog(@"Reload Store");        
    NSURL *storeURL = [[self applicationDocumentsDirectory] URLByAppendingPathComponent:@"Envylope.sqlite"];
    NSDictionary *localStoreOptions = nil;
    localStoreOptions = @{ NSPersistentStoreRemoveUbiquitousMetadataOption : @YES,
                           NSMigratePersistentStoresAutomaticallyOption : @YES,
                           NSInferMappingModelAutomaticallyOption : @YES};

    if (store) {
        [self.persistentStoreCoordinator removePersistentStore:store error:nil];
    }

    [self.persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType
                                                  configuration:nil
                                                            URL:storeURL
                                                        options:localStoreOptions
                                                          error:nil];
    storeURL = [self.persistentStoreCoordinator.persistentStores.lastObject URL];
    NSLog(@"Current Store URL (after iCloud to Local migration): %@", [storeURL description]);

    NSLog(@"Done reloading");

    [[NSUserDefaults standardUserDefaults]setBool:YES forKey:@"MigratedFromiCloudToLocal"];
    [[NSUserDefaults standardUserDefaults]synchronize];

}

Sure, there's some redundant code in there, but the NSLog for before iCloud to Local Migration shows:

 Current Store URL (before iCloud to Local migration): file:///var/mobile/Applications/62CAFCEE-450E-410D-9D56-4918DD70EC07/Documents/CoreDataUbiquitySupport/mobile~98457667-A467-4095-BB7F-EF36EB2B0FE1/EnvyCloud/CD08912E-C135-4056-9557-6B47C84ECEF9/store/Envylope.sqlite

The NSLog for after the data has been migrated shows:

Current Store URL (after iCloud to Local migration): file:///var/mobile/Applications/62CAFCEE-450E-410D-9D56-4918DD70EC07/Documents/Envylope.sqlite

This clearly shows the data has been migrated over to the local store. However, at this point in the console, I'm not sure what to expect, but ideally, this procedure should have the same effect as there not being iCloud enabled on the device at all (if I delete my app, delete my iCloud account on the device and run the app, I get the expected - run the app WITHOUT any iCloud options). This is not happening because it continues to push forward iCloud updates. Without running the app again, data added in the other device syncs across here. With running the app again, the using local storage 1 and 0 appear in the console.

Therefore, the data may have been migrated to a local store, but disabling iCloud within the app seems to have no effect.

I would really appreciate some thoughts on this.

Update: The solution has been found as a combination of both answers below. For anyone following this question to see the next scenario, please refer to this question: Migrating an iCloud Store to a Local Store and making sure the data is there through each app launch

Upvotes: 2

Views: 526

Answers (2)

Jay Versluis
Jay Versluis

Reputation: 2072

It's a two-step process: like @wottle suggests, remove all three observers from your app. Do this once the iCloud data has been migrated to a new local store file:

- (void)removeCloudObservers {

[[NSNotificationCenter defaultCenter]removeObserver:self name:NSPersistentStoreCoordinatorStoresWillChangeNotification object:self.managedObjectContext.persistentStoreCoordinator];
[[NSNotificationCenter defaultCenter]removeObserver:self name:NSPersistentStoreCoordinatorStoresDidChangeNotification object:self.managedObjectContext.persistentStoreCoordinator];
[[NSNotificationCenter defaultCenter]removeObserver:self name:NSPersistentStoreDidImportUbiquitousContentChangesNotification object:self.managedObjectContext.persistentStoreCoordinator];
}

Next, if you're absolutely positive that you want to remove the entire ubiquitous container from iCloud (as we did manually before), you can call a singleton method on the NSPersistentStoreCoordinator class (removeUbiquitousContentAndPersistentStoreAtURL). Grab the iCloud store file in question (I'll call it yourStore) and then use something like this:

- (void)removeCloudContainer:(NSPersistentStore *)yourStore {

NSError *error = nil;
if (![NSPersistentStoreCoordinator removeUbiquitousContentAndPersistentStoreAtURL:yourStore.URL options:yourStore.options error:&error]) {
    NSLog(@"That didn't work so well, and here's why: %@", error.localizedFailureReason);
}
}

This will remove the entire container, and no other devices will be able to access iCloud data anymore. Do this once the iCloud store is removed from the coordinator and when the app is already running happily on the local store.

From a UI point of view, it may be a good idea to let users make this choice after all data on all devices has been migrated to local stores rather than integrate this into the "switch iCloud off" routine - only because it may be tricky to deal with peers that still expect an iCloud store to be there.

Upvotes: 1

wottle
wottle

Reputation: 13619

It seems like you are migrating the data store correctly, but are still getting notified of iCloud modifications. At some point in your code you are registering an observer for iCloud changes, something like

NSUbiquitousKeyValueStore *store = [NSUbiquitousKeyValueStore defaultStore];
[[NSNotificationCenter defaultCenter] addObserver:self
                                     selector:@selector(updateKVStoreItems:)
                                         name:NSUbiquitousKeyValueStoreDidChangeExternallyNotification
                                       object:store];

When you switch to local data, you should remove the observer so that you will no longer get notified on that device when some data changes in the iCloud data store on Apple's end (and be sure to add the observer back if they switch back to iCloud).

[[NSNotificationCenter defaultCenter] removeObserver:self name:NSUbiquitousKeyValueStoreDidChangeExternallyNotification object:nil];

Note that your notification will likely be different if you're using iCloud with a Core Data store instead of simply a key value store like I'm showing.

Upvotes: 1

Related Questions