As I Learn CloudKit Syncing - Part 5: Tracking Game Changes

This is part 5 in a series of posts talking through how I built CloudKit syncing into Qiktionary

In the last installment of this series I discussed how I was tracking newly created games.

Tracking changes to a game that has already been synced with CloudKit has to be approached in a different way.

The first approach I thought about using was based on time. I would track the “last updated” times of all the games and also track the last sync time. Then I could just query for all games with a more recent “last updated” value.

I decided against that approach for a couple of reasons. I was worried I wouldn’t be able to correctly track the last synced date when things went wrong with a sync, thus possibly letting some game updates slip through the cracks. And I also wanted to be a good CloudKit citizen and only send values that had actually changed since the last sync, and not the entire game every time there was a change.

The approach I’m using

I decided to create a table called GameChange that stores the gameID and a set of changedFields that correspond to column names on the Game table. When a game is updated I create a GameChange record with the set of fields that have changed on the game. If there is already a GameChange record for that game, I update the GameChange’s changedFields value with any new fields that have changed on that game.

This makes it really easy to know what games have changed, all I have to do is SELECT * FROM GameChange. Then, after those changes have been sent to CloudKit using the CKModifyRecordsOperation1, I can just delete the GameChange record.

Don’t track changes that don’t need to be synced

It took me a bit to really figure this out but it may seem pretty obvious to you: only track changes to a game that need to be sent to CloudKit. For example, I ignore changes when:

  • Updating the remoteRecordID field.
  • Updating a game with new data that was fetched from CloudKit.

I do this by having two different “save” methods on my Game class, save and saveWithoutTrackingChanges. The save method is inherited from FCModel, and it looks a bit like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
- (BOOL)save
{
    NSArray *changedFieldNames = [self changedFieldNamesWithoutRemoteColumns];

    BOOL existsInDB = self.existsInDatabase;
    BOOL generateChanges = !self.stopSaveChangeGeneration;

    BOOL saved = [super save];

    if (saved && generateChanges && existsInDB) {

        BOOL hasChanges = changedFieldNames.count > 0;

        if (hasChanges) {
            [GameChange saveChangeForGame:self withChangedFields:changedFieldNames];
        }

        return saved;

    }else{
        return saved;
    }
}

Note the call to changedFieldNamesWithoutRemoteColumns which returns an array of field names that have changed on the game record, filtering out the remoteRecordID column that we don’t need to sync with iCloud.

saveWithoutTrackingChanges just sets the stopSaveChangeGeneration flag around a call to save:

1
2
3
4
5
6
7
8
9
10
- (BOOL)saveWithoutTrackingChanges
{
    self.stopSaveChangeGeneration = YES;

    BOOL result = [self save];

    self.stopSaveChangeGeneration = NO;

    return result;
}

The other important piece here is the call to [GameChange saveChangeForGame:self withChangedFields:changedFieldNames], which is implemented like so:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
+ (instancetype)saveChangeForGame:(Game *)game withChangedFields:(NSArray *)changedFields;
{
    GameChange *gameChange = [GameChange firstInstanceWhere:@"gameID = ?", game.gameID];

    if (!gameChange) {
        gameChange = [GameChange new];
        gameChange.gameID = game.gameID;
        gameChange.changedFields = [NSSet setWithArray:changedFields];
    }else{
        NSSet *existingChangedFields = gameChange.changedFields;
        NSMutableSet *mutableChangedFields = [NSMutableSet setWithSet:existingChangedFields];
        [mutableChangedFields addObjectsFromArray:changedFields];
        gameChange.changedFields = mutableChangedFields.copy;
    }

    if ([gameChange save]) {
        return gameChange;
    }else{
        return nil;
    }
}

Bonus: tracking game deletions

I take a similar approach to tracking when a local game is deleted. I have a GameDeletion table that stores the CKRecordID of any game that has been deleted (and has already been synced to iCloud). I don’t keep the local Game record in the database either (with something like a deleted BOOL to mark it as deleted). The Game record is really deleted from the database, along with it’s corresponding RemoteRecord entry.

When I’m setting up my CKModifyRecordsOperation, all I have to do is SELECT * FROM GameDeletion. Then after the operation succeeds, I can delete the GameDeletion record.

Tracking complete

If you recall the syncing recipe is as follows:

  1. Track local changes
  2. Send changes to the server
  3. Resolve conflicts
  4. Fetch server changes with CKFetchRecordChangesOperation
  5. Apply server changes
  6. Save server change token

Now that I’ve tracked all the local changes I need to track, it’ll finally be time to start sending those changes to the server. That I will start covering in my next post.


  1. I’ll eventually get to how I’m using the CKModifyRecordsOperation to send all these local changes to CloudKit.