As I Learn CloudKit Syncing - Part 4: Tracking Local Changes

I’m syncing games. Game data can change in three ways: A new game, a change to an existing game, and a deleted game. All three are tracked differently.

Tracking new games

I can know if a game is new based on whether it is already linked to a CKRecord. According to the docs, the way you link local data to a CKRecord is through encoding the CKRecord’s metadata with the -encodeSystemFieldsWithCoder method on CKRecord.

At first I was saving the encoded metadata along with the game data in the Game table. It was a BLOB column1 called remoteRecord. FCModel will store BLOB data as an instance of NSData. Here is how I turned the CKRecord into NSData to store in the Game table:

NSMutableData *data = [NSMutableData data];
NSKeyedArchiver *archiver = [[NSKeyedArchiver alloc] initForWritingWithMutableData:data];

[instanceValue encodeSystemFieldsWithCoder:archiver];

[archiver finishEncoding];

NSData *remoteRecordData = [NSData dataWithData:data];

And here is how I converted it back from an instance of NSData to a CKRecord:

NSData *data = (NSData *)databaseValue;
NSKeyedUnarchiver *coder = [[NSKeyedUnarchiver alloc] initForReadingWithData:data];
CKRecord *record = [[CKRecord alloc] initWithCoder:coder];
[coder finishDecoding];

This worked well until I implemented the “Fetch server changes with CKFetchRecordChangesOperation” step of the CloudKit Syncing recipe2.

In this step, at some point I need to query the Game table and lookup a specific game, and all I have is a CKRecord instance. I can’t search the remoteRecord BLOB column for the CKRecord because it’s been encoded into a bag of bytes.

I also can’t query the Game table by the gameID (that’s the Game table’s primary key column) which is stored in the CKRecord. Even though the gameID is one of the Game properties I’m saving on the CKRecord. Why?

It has to do with how CKFetchRecordChangesOperation works3. As its name implies, it only pulls in changes. So for example if a game’s record on iCloud has its guesses value changed, then the CKRecord returned by CKFetchRecordChangesOperation will only include the new value for guesses but not any of the other values on that CKRecord.

So the way I’ve worked around this is to create a new table called RemoteRecord that has two fields, a remoteRecordID and a remoteRecord. Now the CKRecord instance is encoded into the remoteRecord column of RemoteRecord, and the local Game table has a remoteRecordID foreign key relationship to the RemoteRecord table.

The thing that makes it possible to query a local Game from a CKRecord is what I’m using as the remoteRecordID. Every CKRecord has a CKRecordID associated with it, and that CKRecordID has a property called recordName which is a string identifier that doesn’t exceed 255 characters. By using the recordName as the value of the remoteRecordID, I can now easily lookup a local Game from a CKRecord, kind of like this:

NSString *recordName = record.recordID.recordName;

Game *existingGame = [Game firstInstanceWhere:@"remoteRecordID = ?", recordName];

I did consider just putting the remoteRecordID column on the Game table and not creating the RemoteRecord table at all, but storing the CKRecord in a seperate table from Game made some things easier when tracking changes to a local game.

So to bring this back around to how I’m tracking new local games, with this setup, all I have to do is query for Games that don’t have a remoteRecordID set, like this:

NSArray *newLocalGames = [Game instancesWhere:@"remoteRecordID IS NULL"];

In the next installment I’m going to cover how I’m tracking local changes to existing games.

Get notified of new posts by following me on Twitter.

  1. I’m using SQLite for my local database, wrapped by FMDB and FCModel

  2. As a reminder about what that recipe is, read Part 1

  3. Definitely going to be digging into this more later.