20

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)

makerofthings7
  • 60,103
  • 53
  • 215
  • 448

2 Answers2

42

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.

Community
  • 1
  • 1
breakingobstacles
  • 2,815
  • 27
  • 24
  • 1
    Does encodeSystemFields remove the need for me to serialize the entire record in my DB? Or if I were to store everything in MySQL, this serialized data as well, is there a chance I might be redundant and wasteful as well? – makerofthings7 Oct 03 '16 at 02:11
  • 3
    @LamonteCristo: `encodeSystemFields` **only** encodes the system metadata values of the CKRecord, **not** any keys+values you set. Thus, you absolutely should record the "data" (fields you set) from the CKRecord in your DB separately - it's not redundant. – breakingobstacles Feb 10 '17 at 17:33
  • This was super helpful. I can’t thank you enough, @breakingobstacles – Clifton Labrum Jun 26 '18 at 06:16
  • 1
    How do you initialize a brand new `CKRecord` in your app since `encodeSystemFields` will be `nil` to start? Can you create a record manually like this? `let record = CKRecord(recordType: "...", recordID: CKRecordID(recordName: "...", zoneID: "..."))` – Clifton Labrum Jun 26 '18 at 19:41
  • @CliftonLabrum Yes, when creating new records that have not been saved to iCloud yet, that is exactly what you do. Then you pass the new record to a `CKModifyRecordsOperation` to save it. If you set the `perRecordCompletionBlock` or `modifyRecordsCompletionBlock` on the operation, you will then have access to the system fields that the server set on the new record. – sc0rp10n Jan 13 '20 at 20:52
3

As of iOS 15 / Swift 5.5 this extension might be helpful:

public extension CKRecord {
    var systemFieldsData: Data {
        let archiver = NSKeyedArchiver(requiringSecureCoding: true)
        encodeSystemFields(with: archiver)
        archiver.finishEncoding()
        return archiver.encodedData
    }

    convenience init?(systemFieldsData: Data) {
        guard let una = try? NSKeyedUnarchiver(forReadingFrom: systemFieldsData) else {
            return nil
        }
        self.init(coder: una)
    }
}
Klaas
  • 22,394
  • 11
  • 96
  • 107
  • as of Swift 5.8, the original answer gives a deprecation warning, this answer is the correct one now, and backwards compatible with Data generated using the original answer. Thanks much Klaas. – Kem Mason Apr 18 '23 at 18:35