Dear Stack Overflow Folks,
I have built up an app using Core Data and iCloud. It's a simple 4-tab in a UITabBarController
, with each tab being a UITableViewController
. The first tab is entitled Timeline, followed by Person, Event and Date.
With Device 1, I add in some entries when "Using local storage: 0
" is displayed in the console. When I build up the second device (on the same iCloud account) (iPad), it starts off with Using Local Storage: 1
(expected), followed by 0
(expected). However, the Timeline tab (which is the main view) does not actually show the new entry, but the Person, Event and Date tab does show it appropriately. If I relaunch the app, it then appears in the Timeline.
I'm using NSFetchedResultsController
for every UITableViewController
and am referencing an excellent tutorial (http://pinkstone.co.uk/how-t-use-icloud-in-your-ios-apps/) and in my Timeline, I have put the following code:
- (void)viewDidLoad
{
[super viewDidLoad];
// add iCloud observers
[[NSNotificationCenter defaultCenter]addObserver:self selector:@selector(storesWillChange) name:NSPersistentStoreCoordinatorStoresWillChangeNotification object:self.managedObjectContext.persistentStoreCoordinator];
[[NSNotificationCenter defaultCenter]addObserver:self selector:@selector(storesDidChange:) name:NSPersistentStoreCoordinatorStoresDidChangeNotification object:self.managedObjectContext.persistentStoreCoordinator];
[[NSNotificationCenter defaultCenter]addObserver:self selector:@selector(mergeContent:) name:NSPersistentStoreDidImportUbiquitousContentChangesNotification object:self.managedObjectContext.persistentStoreCoordinator];
}
#pragma mark - iCloud Methods
- (void)storesWillChange {
NSLog(@"\n\nStores WILL change notification received\n\n");
// disbale UI
[[UIApplication sharedApplication]beginIgnoringInteractionEvents];
// save and reset our context
if (self.managedObjectContext.hasChanges) {
[self.managedObjectContext save:nil];
} else {
[self.managedObjectContext reset];
}
}
- (void)storesDidChange:(NSNotification *)notification {
NSLog(@"\n\nStores DID change notification received\n\n");
// enable UI
[[UIApplication sharedApplication]endIgnoringInteractionEvents];
// update UI
[self.timelineTableView reloadData];
}
- (void)mergeContent:(NSNotification *)notification {
NSLog(@"!Merge Content here!");
[self.managedObjectContext mergeChangesFromContextDidSaveNotification:notification];
}
I am noticing a few strange things.
Edit for Clarity 1) The iPad (2nd device) never runs the storesWillChange or storesDidChange during the initial import, or during any subsequent syncs.
2) The iPad's Person/Event tab are populated, but the Timeline is not. Only if I re-run the app or add a new entry from Device 1 does the iPad's Timeline get updated.
Question
What do I need to do, to get the iPad to show the data in the Timeline tab, when it has connected to iCloud, the very first time? It's not a question of patience because I waited for 43 minutes with no change.
What is the method that gets run on the second device when it's IMPORTANT the data from iCloud? That's my key here. If it's storesDidChange, WHERE should I place that? I have tried placing the notification listener in the viewDidLoad
Timeline View Controller, the applicationDidFinishLaunchingWithOptions
in the App Delegate, and in the NSManagedObjectContext
and it never runs, in any of those cases.
This is an iOS 7 only app.
Edit: Further Updates
I've now reached a point where the iPad (2nd device) still doesn't show the iCloud data, but if I add a new entry on the first device, it then merges content and synchronises across.
Having read the Apple guide many times this morning: https://developer.apple.com/library/prerelease/ios/documentation/DataManagement/Conceptual/UsingCoreDataWithiCloudPG/UsingSQLiteStoragewithiCloud/UsingSQLiteStoragewithiCloud.html#//apple_ref/doc/uid/TP40013491-CH3-SW4 , I can see that it's important to register for the storesDidChange BEFORE the add persistent store takes place. This makes sense and I did this and I'm getting the NSLog out from the storesDidChange method.
However, after the One Time Ubiquity Store Container finishes it's setup, it's supposed to run the storesWillChange notification, as well as the storesDidChange again.
Neither is being run in my case.
With reference to this page, which seems like exactly the issue I'm facing here: http://www.zx81.org.uk/computing/programming/nsfetchedresultscontroller-and-icloud.html, the user re-fetched in the UITableView. I am doing that, but STILL, the data will not appear on the iPad, the first time it is run and synchronied with the iCloud Store.
However because it's a UITabBarController
, the OTHER tabs ARE being updated appropriately. It's just this tab, which is already showing before and after the migration from the fallback to the iCloud store.
Edit: Adding persistentStoreCoordinator
method here
In case the problem is with my persistentStoreCoordinator, I am adding that code here:
- (NSPersistentStoreCoordinator *)persistentStoreCoordinator
{
NSLog(@"Persistent Store");
if (_persistentStoreCoordinator != nil) {
return _persistentStoreCoordinator;
}
NSURL *storeURL = [[self applicationDocumentsDirectory] URLByAppendingPathComponent:@"Envylope.sqlite"];
NSError *error = nil;
// get a reference to your iCloud URL
NSURL *cloudURL = [self grabCloudPath:@"iCloud"];
// setup options depending on migration
NSDictionary *options = nil;
if ([[NSUserDefaults standardUserDefaults]boolForKey:@"OnLatestVersion"]) {
options = @{ NSMigratePersistentStoresAutomaticallyOption : @YES,
NSInferMappingModelAutomaticallyOption : @YES,
NSPersistentStoreUbiquitousContentURLKey: cloudURL, NSPersistentStoreUbiquitousContentNameKey: @"EnvyCloud"};
NSLog(@"Using iCloud from migration");
}
else
{
NSLog(@"Using the other store without the iCloud Migration");
options = @{ NSMigratePersistentStoresAutomaticallyOption : @YES,
NSInferMappingModelAutomaticallyOption : @YES};
}
NSLog(@"Just before adding the persistent store");
_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();
}
NSLog(@"Just after adding the store");
return _persistentStoreCoordinator;
}
The reason I have the if statement and the userDefaults is because I want to check if the user's app needs to be migrated to a new version and to maintain that version when the app is run again. By this, I mean, if I have the App Store version on my device with data and update to this iCloud version, it migrates the data across to the iCloud store. If I run the app again, it doesn't migrate again, it simply uses the iCloud store. This works to synchronise and while I don't believe this is the problem (because the other device DOES pick up the cloud information on the other tabs), it's here for completion.
Any paths to the right direction would be really helpful.
Amit,
I've done some digging and I believe the iCloud part of your app is working fine. The problem appears to be with the fetched results controller which - on some devices - needs to be re-fetched. The easiest way to do this is when you receive the storesDidChange notification. Instead of reloading the table view, try something like this:
- (void)storesDidChange:(NSNotification *)notification {
NSLog(@"\n\nStores DID change notification received\n\n");
// enable UI
[[UIApplication sharedApplication]endIgnoringInteractionEvents];
// update UI
_fetchedResultsController = nil;
[self fetchedResultsController];
}
First you'll nil the variable, then call the custom initialiser again (amend as appropriate for your app). Now the that table view should update fine and show the results from iCloud.
About those missing notifications:
The reason that you don't receive willChange and didChange is likely because you're re-deploying the app without deleting it from the device first. This is not an accurate scenario: if the actual store files don't change, then iOS won't post those notifications. By removing the app before re-deployment, those notifications are posted correctly - and hopefully will update your fetched results controller too.
Hope this helps!