Swift/Cloudkit: After Record Changed, Upload Triggers "Service Record Changed"

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.

Getting a record to insert already exists error for CloudKit

It sounds like you aren't expecting a record to be there and this is an attempt to save the record for the first time. In that case, I think the issue is you are combining a convenience method ".save" with the method of CKModifyRecordsOperation()

So you are effectively double saving here. Everything down to the publicCloudDatabase.add(operation) action sets up the action, and then kicks off the action, saving the record identified in the "let operation = " step.

So if you stop there, then you are good to go.

But because you then call:
publicCloudDatabase.save(progressRecord) { record, error in
///etc

That actually kicks off a separate, completely different save - hence the error!

But because you don't have a completion block on your operation, you never 'see' the results back.

Try adding this this:

operation.modifyRecordsCompletionBlock = { savedRecords, deletedRecordIDs, error in
if let error = error {
//handle error
} else {
//success
}
}

above this row:

publicCloudDatabase.add(operation)

That should hopefully do the trick! (don't forget to kill the .save section)

Check out the Apple documentation as well: https://developer.apple.com/documentation/cloudkit/ckmodifyrecordsoperation

Swift CloudKit CoreData Remote Change Notification also firing for local changes

If this is expected behaviour, I was hoping there would at least be some useful information in the notification itself so I could tell apart whether the change was triggered by an on-device action or synced from CloudKit.

Yes this is expected behaviour, because whether local on device or change from the cloud this is recorded as change in your store/database.

So in order to process changes from the cloud you can subscribe for database changes, read here

App crashes when saving record to CloudKit

I've had this exact same problem when I converted my old Swift 2.x project to Swift 3. They take the CloudKit completion handlers, and instead of converting them to Swift 3 - it leaves it in Swift 2 and then casts it as a CKCompletionHandler for Swift 3. It always causes a crash. Delete the line that says as! (CKRecord?, Error?) -> Void) from the end of your completion handler. Then go back to your actual completion handler and change it to look like this:

publicDatabase.save(record, completionHandler: { (record:CKRecord?, error: Error?) in
// Remove temp file
do {
try FileManager.default.removeItem(atPath: imageFilePath)

} catch {
print("Failed to save record to the cloud: \(error)")
}
}

Basically you just have to change NSError to Error, and you can get rid of the -> void (returns void) line. It's superfluous. Let me know if that works.

CKReference List update

Bon, managed to figure out how to ask the right question and found the answer here.

How to modify CloudKit Reference Lists

Its obvious isn't it.

CKReference *reference = [[CKReference alloc] initWithRecord:connectionRecord action:CKReferenceActionNone];
NSMutableArray *list_a = [record_a[@"connections"] mutableCopy];
if (!list_a) list_a = [NSMutableArray array];
[list_a addObject:reference];
record_a[@"connections"] = list_a;

CoreData with CloudKit: record types not showing in dashboard

Somehow I'd missed this step while creating the NSPersistentStoreDescription:

description.cloudKitContainerOptions = NSPersistentCloudKitContainerOptions(containerIdentifier: "blah")

I'm not yet seeing it in the Dashboard because now I get an error about the DB model having incompatibilities with CloudKit! But that's another story.



Related Topics



Leave a reply



Submit