CKFetchRecordZoneChangesOperation not calling recordWithIDWasDeletedBlock on Shared Record Deletion

328 views Asked by At

I have a record that I share from device A to device B. Device B accepts the share and show the info on Device B's UI. Device B subscribes to Database changes on that shared record using a CKDatabaseSubscription.

I then delete that record on device A which then triggers a notification to Device B about a database change. I then handle that notification on Device B by fetching the DB changes using a CKFetchDatabaseChangesOperation. I then subsequently get the Zone changes using a CKFetchRecordZoneChangesOperation.

I get calls to recordZoneFetchCompletionBlock and fetchRecordZoneChangesCompletionBlock but that is it. Neither recordChangedBlock nor recordWithIDWasDeletedBlock is called.

It is important to note I'm using this exact same code to monitor shared records of another type and it works. It's actually a record that is a child of a child in this record. So I'm not sure why this top level/parent record would not show as being deleted when queried. The record does delete from CloudKit when Device A deletes it (both on its private DB and on the shared DB of DeviceB). So it's not a relationship or restriction of any kind on CloudKit I don't believe.

Welcome any thoughts how to chase this one down. Been at this for a week.

This is the code I'm using to subscribe...

+ (void) subscribeToGameChangesInDB:(int)dbID success:(void (^) (void))success failure:(void (^)(NSError *error))failure
{
    CKNotificationInfo *notif = [[CKNotificationInfo alloc] init];

    notif.alertLocalizationKey = @"DB Changed";
    notif.shouldBadge = NO;
    notif.shouldSendContentAvailable = YES;

    CKDatabase *db = [CloudKitManager getDBForIdentifier:dbID];

    if (dbID == kCKDBPublic)
    {
        NSLog(@"ERROR : Cannot subscribe to database changes in the PUBLIC DB.");
    }
    else
    {
        CKDatabaseSubscription *subscription = [[CKDatabaseSubscription alloc] initWithSubscriptionID:[NSString stringWithFormat:@"AllDBhanges-%@", [CloudKitManager getDatabaseNameForScope:db.databaseScope]]];

        subscription.notificationInfo = notif;

        [CloudKitManager saveSubscription:subscription inDB:db success:success failure:failure];
    }
}

+ (void) saveSubscription:(CKSubscription*)subscription inDB:(CKDatabase*)db success:(void (^) (void))success failure:(void (^)(NSError *error))failure
{
    [db saveSubscription:subscription completionHandler:^(CKSubscription * _Nullable subscription, NSError * _Nullable error)
     {
         if (error)
         {
             if ((error.code == CKErrorServerRejectedRequest) || (error.code == CKErrorUnknownItem)) // We probably already subscribed from another device.
             {
                 NSLog(@"Already Subscribed... Passing Success. (code:%ld)", (long)error.code);
                 success ();
             }
             else
             {
                 failure (error);
             }
         }
         else
         {
             success();
         }
     }];
}

Here is the code I'm using to handle the notification and do the two operations above.

- (void) handleDatabaseNotification:(CKDatabaseNotification*)dbNotif
{
    CKDatabase *db = [self getDatabaseForNotificationDatabaseScope:dbNotif.databaseScope];
            
    [self handleDatabase:db changesWithPreviousChangeToken:[self getServerDBChangeTokenFromDisk] withCompletion:^(NSError * _Nonnull error) {

        if (error)
        {
            NSLog (@"ERROR : HAndling Database change notificiation : %@", error);
        }
    }];
}

- (void) handleDatabase:(CKDatabase*)db changesWithPreviousChangeToken:(CKServerChangeToken*)previousChangeToken withCompletion:(void (^)(NSError *error))completion
{
    CKFetchDatabaseChangesOperation *operation = [[CKFetchDatabaseChangesOperation alloc] initWithPreviousServerChangeToken:previousChangeToken];

    [operation setRecordZoneWithIDChangedBlock:^(CKRecordZoneID * _Nonnull zoneID) {

        NSLog(@"ZONE CHANGE : %@", zoneID);

        [self handleDatabase:db zoneID:zoneID changesWithPreviousChangeToken:[self getServerZonesChangeTokenFromDisk]];
    }];

    [operation setFetchDatabaseChangesCompletionBlock:^(CKServerChangeToken * _Nullable serverChangeToken, BOOL moreComing, NSError * _Nullable operationError) {

        if (operationError)
        {
            NSLog (@"ERROR : Unable to fetch Zone changes : %@", operationError);
            
            completion(operationError);
        }

        NSLog(@"New serverChangeToken : %@", serverChangeToken);
                    
        [self storeServerDBChangeTokenToDisk:serverChangeToken];
        
        if (moreComing)
        {
            [self handleDatabase:db changesWithPreviousChangeToken:serverChangeToken withCompletion:completion];
        }
        else
            completion (nil);
    }];

    [db addOperation:operation];
}

- (void) handleDatabase:(CKDatabase*)db zoneID:(CKRecordZoneID*)zoneID changesWithPreviousChangeToken:(CKServerChangeToken*)previousChangeToken
{
    NSLog(@"Zone Changes. DB : %@\nzoneID : %@", db, zoneID);

    CKFetchRecordZoneChangesConfiguration *config = [[CKFetchRecordZoneChangesConfiguration alloc] init];

    config.previousServerChangeToken = previousChangeToken;

    CKFetchRecordZoneChangesOperation *zoneOperation = [[CKFetchRecordZoneChangesOperation alloc] initWithRecordZoneIDs:@[zoneID] configurationsByRecordZoneID:@{zoneID: config}];
    
    [zoneOperation setRecordZoneFetchCompletionBlock:^(CKRecordZoneID * _Nonnull recordZoneID, CKServerChangeToken * _Nullable serverChangeToken, NSData * _Nullable clientChangeTokenData, BOOL moreComing, NSError * _Nullable recordZoneError) {
    
        if (recordZoneError)
        {
            NSLog(@"ERROR : recordZoneError : %@", recordZoneError);
        }
        else
        {
            [self storeServerZonesChangeTokenToDisk:serverChangeToken];

            if (moreComing)
            {
                NSLog(@"*********** There's MORE COMING ***************");
                
                [self handleDatabase:db zoneID:zoneID changesWithPreviousChangeToken:serverChangeToken];
            }
            
            /* Handle changes */
        }
    }];
    
    [zoneOperation setRecordChangedBlock:^(CKRecord * _Nonnull record) {
        
        [self handleDatabaseRecordChange:record];
        
    }];
    
    [zoneOperation setFetchRecordZoneChangesCompletionBlock:^(NSError * _Nullable operationError) {

        NSLog (@"setFetchRecordZoneChangesCompletionBlock");
        
        if (operationError)
            NSLog (@"setFetchRecordZoneChangesCompletionBlock ERROR : %@", operationError);

    }];
    
    [zoneOperation setRecordZoneChangeTokensUpdatedBlock:^(CKRecordZoneID * _Nonnull recordZoneID, CKServerChangeToken * _Nullable serverChangeToken, NSData * _Nullable clientChangeTokenData) {
        
        NSLog(@"setRecordZoneChangeTokensUpdatedBlock Called : \n\nserverChangeToken = %@\n\nclientChangeTokenData = %@\n", serverChangeToken, clientChangeTokenData);

    }];

    [zoneOperation setRecordWithIDWasDeletedBlock:^(CKRecordID * _Nonnull recordID, CKRecordType  _Nonnull recordType) {

        NSManagedObjectContext *context = [self.coreManager getContext];

        [context performBlockAndWait:^{

            [self handleDeleteRecordID:recordID recordType:recordType];

            [self.coreManager deleteContext:context];
        }];
    }];

    [db addOperation:zoneOperation];
}
2

There are 2 answers

0
Clifton Labrum On

It's been a while since I ran into this issue, so the details are fuzzy. But I opened a support ticket with an Apple engineer about this.

The net of it (this was like 2018) was that when a parent record of a CKShare is deleted, and that record was shared with you, you will only get a database change notification. Your access to the record has ended, so there is no record-level info about it.

I asked the engineer if there was a way to know if the record was deleted or unshared or why it changed, and he said there wasn't a way to know.

So I think the way to handle this is to just infer that the record is not accessible and then re-query for it. You'll then know if it's available or not.

0
Stamenkovski On

Let's say you have zone with id "John", and you have only one record in it and if that record is deleted you will not receive recordWithIDWasDeletedBlock but you will receive recordZoneWithIDWasDeletedBlock.

This means that this zone "John" doesn't have any more records and it does not exist anymore so it is deleted.

But if you have more records in zone "John" and you delete one record from it, then you will receive recordWithIDWasDeletedBlock.