icloudcloudkitios10icloud-api

How (and when) do I use iCloud's encodeSystemFields method on CKRecord?


encodeSystemFields is supposed to be used when I keep records locally, in a database.

Once I export that data, must I do anything special when de-serializing it?

What scenarios should I act upon information in that data?

As a variation (and if not covered in the previous question), what does this information help me guard against? (data corruption I assume)


Solution

  • encodeSystemFields is useful to avoid having to fetch a CKRecord from CloudKit again to update it (barring record conflicts).

    The idea is:

    When you are storing the data for a record retrieved from CloudKit (for example, retrieved via CKFetchRecordZoneChangesOperation to sync record changes to a local store):

    1.) Archive the CKRecord to NSData:

    let record = ...
    
    // archive CKRecord to NSData
    let archivedData = NSMutableData()
    let archiver = NSKeyedArchiver(forWritingWithMutableData: archivedData)
    archiver.requiresSecureCoding = true
    record.encodeSystemFieldsWithCoder(with: archiver)
    archiver.finishEncoding()
    

    2.) Store the archivedData locally (for example, in your database) associated with your local record.

    When you want to save changes made to your local record back to CloudKit:

    1.) Unarchive the CKRecord from the NSData you stored:

    let archivedData = ... // TODO: retrieved from your local store
    
    // unarchive CKRecord from NSData
    let unarchiver = NSKeyedUnarchiver(forReadingWithData: archivedData)  
    unarchiver.requiresSecureCoding = true 
    let record = CKRecord(coder: unarchiver)
    

    2.) Use that unarchived record as the base for your changes. (i.e. set the changed values on it)

    record["City"] = "newCity"
    

    3.) Save the record(s) to CloudKit, via CKModifyRecordsOperation.


    Why?

    From Apple:

    Storing Records Locally

    If you store records in a local database, use the encodeSystemFields(with:) method to encode and store the record’s metadata. The metadata contains the record ID and change tag which is needed later to sync records in a local database with those stored by CloudKit.

    When you save changes to a CKRecord in CloudKit, you need to save the changes to the server's record.

    You can't just create a new CKRecord with the same recordID, set the values on it, and save it. If you do, you'll receive a "Server Record Changed" error - which, in this case, is because the existing server record contains metadata that your local record (created from scratch) is missing.

    So you have two options to solve this:

    1. Request the CKRecord from CloudKit (using the recordID), make changes to that CKRecord, then save it back to CloudKit.

    2. Use encodeSystemFields, and store the metadata locally, unarchiving it to create a "base" CKRecord that has all the appropriate metadata for saving changes to said CKRecord back to CloudKit.

    #2 saves you network round-trips*.

    *Assuming another device hasn't modified the record in the meantime - which is also what this data helps you guard against. If another device modifies the record between the time you last retrieved it and the time you try to save it, CloudKit will (by default) reject your record save attempt with "Server Record Changed". This is your clue to perform conflict resolution in the way that is appropriate for your app and data model. (Often, by fetching the new server record from CloudKit and re-applying appropriate value changes to that CKRecord before attempting the save again.)

    NOTE: Any time you save/retrieve an updated CKRecord to/from CloudKit, you must remember to update your locally-stored archived CKRecord.