0

I have a Core Data application with which I am trying to integrate the Ensembles framework using iCloud as my backend. I have most things working except that when making a change on one device, I have to make a change and save the context on the other device for it to pick up the remote changes.

The tableview that reflects the data conforms to NSFetchedResultsControllerDelegate. When the local data changes and it picks up the remote changes, the remote changes are reflected correctly.

Implementing a "Sync" button, which manually calls syncWithCompletion (below) does not pick up the changes.

A timer that fires off every two minutes, calling syncWithCompletion, does not pick up the changes.

Turning sync off and then on again does pick up the changes.

Restarting the app does not pick up the changes.

#pragma mark - ENSEMBLES

- (void)setupEnsembles {
  if (ensemblesDataDebug==1) { printf("Running %s\n", [NSStringFromSelector(_cmd) UTF8String]); }

  // set the sync UI on
  [[NSNotificationCenter defaultCenter] postNotificationName:@"setSyncUIOn" object:nil];

  // setup ensemble
  self.cloudFileSystem = [[CDEICloudFileSystem alloc] initWithUbiquityContainerIdentifier:nil];
  self.ensemble = [[CDEPersistentStoreEnsemble alloc] initWithEnsembleIdentifier:@"RecordStore"
                                                              persistentStoreURL:self.storeURL
                                                           managedObjectModelURL:[self modelURL]
                                                                 cloudFileSystem:self.cloudFileSystem];
  self.ensemble.delegate = self;

  // Listen for local saves, and trigger merges
  [[NSNotificationCenter defaultCenter] addObserver:self
                                           selector:@selector(localSaveOccurred:)
                                               name:CDEMonitoredManagedObjectContextDidSaveNotification
                                             object:nil];

  [[NSNotificationCenter defaultCenter] addObserver:self
                                           selector:@selector(cloudDataDidDownload:)
                                               name:CDEICloudFileSystemDidDownloadFilesNotification
                                             object:nil];

  [self syncWithCompletion:NULL];

  // configure a timer to trigger a merge every two minutes
  if (!self.ensemblesSyncTimer) {
    self.ensemblesSyncTimer = [NSTimer scheduledTimerWithTimeInterval:120.0
                                                               target:self
                                                             selector:@selector(performScheduledSync:)
                                                             userInfo:nil
                                                              repeats:YES];
  }
}

- (void)performScheduledSync:(NSTimer*)aTimer {
  if (ensemblesDataDebug==1) { printf("Running %s\n", [NSStringFromSelector(_cmd) UTF8String]); }
  [self syncWithCompletion:NULL];
}

- (void)syncWithCompletion:(void(^)(void))completion {
  if (ensemblesDataDebug==1) { printf("Running %s\n", [NSStringFromSelector(_cmd) UTF8String]); }

  // set the sync UI on
  [[NSNotificationCenter defaultCenter] postNotificationName:@"setSyncUIOn" object:nil];

  // this checks to make sure there is an ensemble, because this method
  // can be called without knowing whether ensembles is enabled or not
  if (self.ensemble) {
    if (coreDataDebug==1) { NSLog(@"there is an ensemble, going to leech or merge"); }

    if (!self.ensemble.isLeeched) {
      if (coreDataDebug==1) { NSLog(@"leeching"); }
      [self.ensemble leechPersistentStoreWithCompletion:^(NSError *error) {
        if (error) NSLog(@"Error in leech: %@", [error localizedDescription]);

        // set the last synced date
        NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
        dateFormatter.timeStyle = NSDateFormatterMediumStyle;
        dateFormatter.dateStyle = NSDateFormatterMediumStyle;
        [dateFormatter setLocale:[NSLocale currentLocale]];
        [[NSUserDefaults standardUserDefaults] setObject:[dateFormatter stringFromDate:[NSDate date]]
                                                  forKey:@"iCloudLastSyncDate"];

        [[NSNotificationCenter defaultCenter] postNotificationName:@"setSyncUIOff" object:nil];
                                                            object:nil];
        if (completion) {completion();}
      }];
    }
    else {
      if (coreDataDebug==1) { NSLog(@"merging"); }
      [self.ensemble mergeWithCompletion:^(NSError *error) {
        if (error) NSLog(@"Error in merge: %@", [error localizedDescription]);

        // set the last synced date
        NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
        dateFormatter.timeStyle = NSDateFormatterMediumStyle;
        dateFormatter.dateStyle = NSDateFormatterMediumStyle;
        [dateFormatter setLocale:[NSLocale currentLocale]];
        [[NSUserDefaults standardUserDefaults] setObject:[dateFormatter stringFromDate:[NSDate date]]
                                                  forKey:@"iCloudLastSyncDate"];

        [[NSNotificationCenter defaultCenter] postNotificationName:@"setSyncUIOff" object:nil];
        [[NSNotificationCenter defaultCenter] postNotificationName:@"recordCollectionNeedsRefresh"
                                                            object:nil];
        if (completion) {completion();}
      }];
    }
  }
}

- (void)localSaveOccurred:(NSNotification *)notif {
  if (ensemblesDataDebug==1) { printf("Running %s\n", [NSStringFromSelector(_cmd) UTF8String]); }
  [self syncWithCompletion:NULL];
}

- (void)cloudDataDidDownload:(NSNotification *)notif {
  if (ensemblesDataDebug==1) { printf("Running %s\n", [NSStringFromSelector(_cmd) UTF8String]); }
  [self syncWithCompletion:NULL];
}

- (void)persistentStoreEnsemble:(CDEPersistentStoreEnsemble *)ensemble didSaveMergeChangesWithNotification:(NSNotification *)notification {
  if (ensemblesDataDebug==1) { printf("Running %s\n", [NSStringFromSelector(_cmd) UTF8String]); }

  [_context performBlockAndWait:^{
    [_context mergeChangesFromContextDidSaveNotification:notification];
  }];
}

- (NSArray *)persistentStoreEnsemble:(CDEPersistentStoreEnsemble *)ensemble globalIdentifiersForManagedObjects:(NSArray *)objects {
  if (ensemblesDataDebug==1) { printf("Running %s\n", [NSStringFromSelector(_cmd) UTF8String]); }
  return [objects valueForKeyPath:@"uniqueIdentifier"];
}

- (void)removeEnsembles {
  if (ensemblesDataDebug==1) { printf("Running %s\n", [NSStringFromSelector(_cmd) UTF8String]); }
  [self disconnectFromSyncServiceWithCompletion:NULL];
}

- (void)disconnectFromSyncServiceWithCompletion:(CDECodeBlock)completion {
  if (ensemblesDataDebug==1) { printf("Running %s\n", [NSStringFromSelector(_cmd) UTF8String]); }
  [self.ensemble deleechPersistentStoreWithCompletion:^(NSError *error) {

    self.ensemble.delegate = nil;
    [self.ensemble dismantle];
    self.ensemble = nil;
    [[NSUserDefaults standardUserDefaults] removeObjectForKey:@"iCloudLastSyncDate"];
    [[NSUserDefaults standardUserDefaults] setBool:NO forKey:@"iCloudSyncingEnabled"];
    [self.ensemblesSyncTimer invalidate];
    self.ensemblesSyncTimer = nil;

  if (completion) completion();
  }];
}

- (void)persistentStoreEnsemble:(CDEPersistentStoreEnsemble *)ensemble didDeleechWithError:(NSError *)error {
  if (ensemblesDataDebug==1) { printf("Running %s\n", [NSStringFromSelector(_cmd) UTF8String]); }
  NSLog(@"Store did deleech with error: %@", error);
}

Any ideas where I'm going wrong?

[EDIT since my comment is too long]

Firstly, didSaveMergeChangesWithNotification does not get called if I do a local save and there are changes in the cloud (assuming they have propagated - is there a way to know? I've waited quite a while to try to rule that out), nor does it get called when I trigger a manual sync. It is only called when I make a change to a local model and then save my context. I don't quite know where that leaves me. Secondly, checking the fetch controller, the changes in the cloud are indeed not pulled down. I've turned on CDELoggingLevelVerbose to continue investigating, but I know there's something I'm doing fundamentally wrong that I must be missing.

Also, here is a big, big change - I've just realized from an old issue in the Ensembles Github that triggering an iCloud sync in the simulator actually works! Unfortunately, I'm doing all my testing in the simulator as I don't have any devices (I burned my iPhone with too many iCloud logins during testing). Could this be it? Could I be confident that this is actually working normally, but there is something in the simulator that is not actually letting iCloud sync trigger?

user4034838
  • 121
  • 9
  • Are you hit by the dreaded "cloud to butt" extension, or have you made a custom backend called "ibutt"? If you are simply using iCloud Drive, one possible explanation is simply that it is taking a while to sync up its files. That's something Ensembles can't really influence, other than requesting they be downloaded. – Drew McCormack Dec 18 '17 at 07:33
  • @DrewMcCormack Hah, sorry, yes, I did have "cloud to butt" installed. I edited the question and I'll be trying out your suggestions in your answer in a minute. Usually I find that extension funny. Not today. – user4034838 Dec 18 '17 at 14:12
  • If you are testing in the simulator, it is indeed possible that it is not properly downloading files. I have heard reports from users having trouble in the simulator. You can try the Xcode Debug menu to trigger an iCloud sync of files. Short of testing on a device, or with Dropbox, I can only suggest Ensembles 2, which uses CloudKit, but it has a fee. If you can get hold of an iPod touch, maybe that is OK for testing. – Drew McCormack Dec 19 '17 at 07:25

1 Answers1

1

It's not clear to me why it is not working, but there are some things you can try to find out.

First, try to figure out from your logs what is different when you do a local save as opposed to just a merge (by pressing the sync button). Does the didSaveMergeChangesWithNotification: delegate method get triggered in both cases? Assuming there are changes in the cloud, it should.

It's also worth checking the fetch results controller. It is possible the changes do enter the store, but that the fetch controller doesn't pick them up. One way to check is to call performFetch and reload your UI at the end of each merge, just to test if that could be the problem.

Another way to see if Ensembles is actually getting and merging the data is to turn on the verbose logging. Use the function CDESetCurrentLogLevel, and pass in CDELoggingLevelVerbose. That will print a lot of information about what the framework is doing, and should give clues.

Drew McCormack
  • 3,490
  • 1
  • 19
  • 23