Using notifications to keep changes in sync across multiple devices

I am writing an app and using CloudKit. The app stores its data in the public database. I’ve created a local cache so that my app can function even if the network isn’t available. I’m using

CKSubscriptions
and the resulting push notifications to keep changes from the cloud in sync. All of this is working well.


Now, if a user has multiple devices and is running my app on all those devices, then it seems that I can no longer mark notifications as “read” (using

CKMarkNotificationsReadOperation
) since I won’t know when all the devices have processed them. This is particularly true if one of the devices is offline when a change happens. If I do mark the notifications as read, then when the other devices check for new notifications (using
CKFetchNotificationChangesOperation
) they will not see them and their local cache will be out of date.


My current solution is to just leave all notifications in an “unread” state and rely on the

CKServerChangeToken
in
CKFetchNotificationChangesOperation
so that each device only grabs the notifications that have occurred since that device last checked. This works OK.


But, it seems to me that since I’m not marking any notifications as “read” they will simply continue to pile up on the server. Maybe this isn’t a big deal, but they will take up space and I have no good way to get rid of them. Over time, this seems as if it could be a problem.


As an example, I just did a test with 3 devices. 1st device was on WiFi and the other 2 were in airplane mode. They were all signed in to same iCloud account. I made 3 changes using iCloud Dashboard. The 1st device got the notifications properly and updated its data. It then marked the notifications as read. I turned off airplane mode on the 2nd device. It got notified that the prior notifications had been read:

notificationType
of
ReadNotification
, but did not receive the change notifications. 3rd device was the same. It seems to me that each device should get the notifications and should be able to mark them as read individually.


Has anyone used subscriptions/notifications in a similar way and come up with a different approach? Also, any feedback on my approach would be welcomed.


Thanks.

I don't use notifications. I call a fetch with the last token each time the app enters foreground as described in the code below. If you wish, you could do this and also respond to a notification when your app is active - although that response would apply only when one account has two devices both running the app -overkill IMHO. But then you can clear the notification without needing to worry about it since all active apps will get it.


- (void)applicationWillEnterForeground:(UIApplication *)application {
      [self fetchTheRides];
//  etc..
}


- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo {
    // I don't do this - it's too responsive.  But if you want:
           //  check notification and if appropriate,  
             //      [self fetchTheRides];
             //       mark notification as read
}
   
-(void)fetchTheRides{
    CKRecordZoneID *ridesZoneID=[[CKRecordZoneID alloc] initWithZoneName:@"RidesZone" ownerName:CKOwnerDefaultName];
    CKFetchRecordChangesOperation *getRides=[[CKFetchRecordChangesOperation alloc] initWithRecordZoneID:ridesZoneID previousServerChangeToken:[parameters objectForKey:@"RidesToken"]];
    __weak CKFetchRecordChangesOperation *weakGetRides=getRides;  // because I need to check it inside block
    getRides.qualityOfService=NSQualityOfServiceUserInitiated;
    getRides.recordChangedBlock=^(CKRecord *aRide){
       //handle aRide having changed
    };
    getRides.fetchRecordChangesCompletionBlock=^(CKServerChangeToken *serverChangeToken, NSData *clientChangeTokenData, NSError *operationError){
        if(!weakGetRides.moreComing){  /
            dispatch_async(dispatch_get_main_queue(), ^{
               // all done   
                [[NSNotificationCenter defaultCenter] postNotificationName:@"RidesWereUpdated" object:nil];
            });
        }
     
        if(operationError){
             //handle error
        }else{
            [parameters setObject:serverChangeToken forKey:@"RidesToken"];
            if(weakGetRides.moreComing){
                [self fetchTheRides];
            }else{
                [parameters setValue:[NSNumber numberWithBool:YES] forKey:@"iCloudAvailable"];
                dispatch_async(dispatch_get_main_queue(), ^{
                      [UIApplication sharedApplication].networkActivityIndicatorVisible = NO;
                });
            }
        }
    };
    [[[CKContainer defaultContainer] privateCloudDatabase] addOperation:getRides];

Thanks very much for your reply. Unfortunately, I cannot use your approach since I'm using the public database (I need to track changes made by multiple users). The CKFetchRecordChangesOperation will only work with custom zones, which are only available in the private database, if I understand it correctly. Otherwise, your suggestion would be perfect for me.


If you have other suggestions, or believe my understanding is incorrect, please let me know. I'd really appreicate it.

Ok, so divide up the problem into


1) synching between different iCloud Accounts

and

2) synching all devices under the same iCloud Account


Accomplish #1 as you suggest to synch a file in the public database with a file in the private database for an iCloud Account. Do that for each iCloud Account that wants to synch to the file in the public database. You can clear the subscription after you read it. (Note - subscriptions are unique for each iCloud Account so clearing the subscription only ckears it for those devices under that iCloud Account)). Also synch the local files on a specific device when that device updates the file in its private database and reset its token as I describe above.


Use my approach to synch that file, now updated in the private database, among all the other devices under that iCloud Account.


QED.

Thanks. I'll dig in to that approach a bit. It might get tricky since any device that is online will likely get the public DB nofiticaion and I'll have to make sure they both don't update the private database.


I've also toyed with the idea of tracking the notifications on a per-device basis myself (in the private database), and then marking them as read when I know that all the devices have processed them. That way I don't have to have record data in three places (public, private, local).


It still would be nice if CloudKit managed per-device notifications in a better way. That would make all of this easier.


I appreicate your suggestions.

Keep the data only in one file in the public database. When it changes send out a notification to all subscribed devices. When an active device receives a notification it synchs and clears the notification. When a device enters foreground it searches for unread notifications and, if there, synchs and clears. Any device that responds to a notification also changes a private database file - just an inconsequential change will do. When a device enters foreground it checks the private database file for changes. If it detects a change it synchs with the public file but does not update the private file. The only duplication will then be if multiple devices on the same iCloud Account respond to the notification. That will cause the first of those devices to needlessly resynch the next time they enter foreground. No real problem.

Using notifications to keep changes in sync across multiple devices
 
 
Q