Swift / CloudKit: After record changed, upload triggers "Service Record Changed"

It looks like you are creating a new CKRecord every time you save.

CloudKit is returning ServerRecordChanged to tell you that a record with the same recordID already exists on the server, and your attempt to save was rejected because the server record's version was different.

Each record has a change tag that allows the server to track when that record was saved. When you save a record, CloudKit compares the change tag in your local copy of the record with the one on the server. If the two tags do not match—meaning that there is a potential conflict—the server uses the value in the [savePolicy property of CKModifyRecordsOperation] to determine how to proceed.

Source: CKModifyRecordsOperation Reference

Although you are using the CKDatabase.saveRecord convenience method, this still applies. The default savePolicy is ifServerRecordUnchanged.

First, I would suggest transitioning to CKModifyRecordsOperation, especially if you are saving multiple records. It gives you much more control over the process.

Second, you need to make changes to the CKRecord from the server, when saving changes to an existing record. You can accomplish this by any of the following:

  1. Requesting the CKRecord from CloudKit, making changes to that CKRecord, and then saving it back to CloudKit.
  2. Storing the saved CKRecord (the one returned in the completion block after saving) using the advice in the CKRecord Reference, persisting this data, and then unarchiving it to get a CKRecord back that you can modify and save to the server. (This avoids some network round-trips to request the server CKRecord.)

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.

let record = ...

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

// unarchive CKRecord from NSData
let unarchiver = NSKeyedUnarchiver(forReadingWithData: archivedData)  
unarchiver.requiresSecureCoding = true 
let unarchivedRecord = CKRecord(coder: unarchiver)

Source: CloudKit Tips and Tricks - WWDC 2015

Keep in mind: you can still encounter the ServerRecordChanged error if another device saves a change to the record after you requested it / last saved it & stored the server record. You need to handle this error by getting the latest server record, and re-applying your changes to that CKRecord.