Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Swift CloudKit SaveRecord "Error saving record"

I am trying to save a record to CloudKit but am getting an error. I had seen elsewhere that this was an issue that required knowing how to save but I can't get this to work.

    var database:CKDatabase = CKContainer.defaultContainer().publicCloudDatabase
    var aRecord:CKRecord!

    if self.cloudId == nil {
        var recordId:CKRecordID = CKRecordID(recordName: "RecordId")
        self.cloudId = recordId // Setup at top
    }

    aRecord = CKRecord(recordType: "RecordType", recordID: self.cloudId)
    aRecord.setObject(self.localId, forKey: "localId")

    // Set the normal names etc
    aRecord.setObject(self.name, forKey: "name")

    var ops:CKModifyRecordsOperation = CKModifyRecordsOperation()
    ops.savePolicy = CKRecordSavePolicy.IfServerRecordUnchanged

    database.addOperation(ops)
    database.saveRecord(aRecord, completionHandler: { (record, error) in

        if error != nil {
            println("There was an error \(error.description)!")

        } else {
            var theRecord:CKRecord = record as CKRecord
            self.cloudId = theRecord.recordID
        }
    })

This gives me the error:

There was an error <CKError 0x16d963e0: "Server Record Changed" (14/2017); "Error saving record <CKRecordID: 0x15651730; xxxxxx:(_defaultZone:__defaultOwner__)> to server: (null)"; uuid = 369226C6-3FAF-418D-A346-49071D3DD70A; container ID = "iCloud.com.xxxxx.xxxx-2">!

Not sure, given that I have added CKModifyRecordsOperation. Sadly there is no examples within Apple's documentation. I miss that (which you get on MSDN).

Thanks peeps!

like image 938
Bren Gunning Avatar asked Jul 06 '14 21:07

Bren Gunning


3 Answers

A record can be saved to iCloud using CKDatabase's convenience method saveRecord: or via a CKModifyRecordsOperation. If it's a single record, you can use saveRecord: but will need to fetch the record you'd like to modify using fetchRecordWithID: prior to saving it back to iCloud. Otherwise, it will only let you save a record with a new RecordID. More here.

database.fetchRecordWithID(recordId, completionHandler: { record, error in
    if let fetchError = error {
            println("An error occurred in \(fetchError)")
        } else {
            // Modify the record
            record.setObject(newName, forKey: "name")
        } 
}


database.saveRecord(aRecord, completionHandler: { record, error in
    if let saveError = error {
            println("An error occurred in \(saveError)")
        } else {
            // Saved record
        } 
}

The code above is only directionally correct but won't work as is because by the time the completionHandler of fetchRecordWithID returns, saveRecord will have fired already. A simple solution would be to nest saveRecord in the completionHandler of fetchRecordWithID. A probably better solution would be to wrap each call in a NSBlockOperation and add them to an NSOperationQueue with saveOperation dependent on fetchOperation.

This part of your code would be for a CKModifyRecordsOperation and not needed in case you are only updating a single record:

var ops:CKModifyRecordsOperation = CKModifyRecordsOperation()
ops.savePolicy = CKRecordSavePolicy.IfServerRecordUnchanged
database.addOperation(ops)

If you do use a CKModifyRecordsOperation instead, you'll also need to set at least one completion block and deal with errors when conflicts are detected with existing records:

let saveRecordsOperation = CKModifyRecordsOperation()

var ckRecordsArray = [CKRecord]()
// set values to ckRecordsArray

saveRecordsOperation.recordsToSave = ckRecordsArray
saveRecordsOperation.savePolicy = .IfServerRecordUnchanged
saveRecordsOperation.perRecordCompletionBlock { record, error in
    // deal with conflicts
    // set completionHandler of wrapper operation if it's the case
}

saveRecordsOperation.modifyRecordsCompletionBlock { savedRecords, deletedRecordIDs, error in
    // deal with conflicts
    // set completionHandler of wrapper operation if it's the case
}

database.addOperation(saveRecordsOperation)

There isn't much sample code yet besides the CloudKitAtlas demo app, which is in Objective-C. Hope this helps.

like image 173
Guto Araujo Avatar answered Nov 06 '22 13:11

Guto Araujo


Generally speaking, you have unitary methods (like saveRecord), which deal with only one record at a time, and mass operations (like CKModifyRecordsOperation), which deal with several records at the same time.

These save operations can be used to save records, or to update records (that is, fetch them, apply changes to them, and then save them again).


SAVE examples:

You create a record and want to save it to CloudKit DB:

let database = CKContainer.defaultContainer().publicCloudDatabase
var record = CKRecord(recordType: "YourRecordType")
database.saveRecord(record, completionHandler: { (savedRecord, saveError in
  if saveError != nil {
    println("Error saving record: \(saveError.localizedDescription)")                
  } else {
    println("Successfully saved record!")
  }
})

You create a bunch of records and you want to save them all at once:

let database = CKContainer.defaultContainer().publicCloudDatabase

// just an example of how you could create an array of CKRecord
// this "map" method in Swift is so useful    
var records = anArrayOfObjectsConvertibleToRecords.map { $0.recordFromObject }

var uploadOperation = CKModifyRecordsOperation(recordsToSave: records, recordIDsToDelete: nil)
uploadOperation.savePolicy = .IfServerRecordUnchanged // default
uploadOperation.modifyRecordsCompletionBlock = { savedRecords, deletedRecordsIDs, error in
  if error != nil {
      println("Error saving records: \(error.localizedDescription)")                            
  } else {
      println("Successfully saved records")
  }
}
database.addOperation(uploadOperation)

UPDATE examples:

Usually, you have 3 cases in which you want to update records :

  1. you know the record identifier (generally the recordID.recordName of the record you want to save: in that case, you will use methods fetchRecordWithID and then saveRecord
  2. you know there is a unique record to update but you don't know its recordID: in that case, you will use a query with method performQuery, select the (only) one you need and again saveRecord

  3. you are dealing with many records that you want to update: in that case, you will use a query to fetch them all (performQuery), and a CKModifyRecordsOperation to save them all.

Case 1 - you know the unique identifier for the record you want to update:

    let myRecordName = aUniqueIdentifierForMyRecord
    let recordID = CKRecordID(recordName: myRecordName)

    database.fetchRecordWithID(recordID, completionHandler: { (record, error) in
        if error != nil {
            println("Error fetching record: \(error.localizedDescription)")
        } else {
            // Now you have grabbed your existing record from iCloud
            // Apply whatever changes you want
            record.setObject(aValue, forKey: attributeToChange)

            // Save this record again
            database.saveRecord(record, completionHandler: { (savedRecord, saveError) in
                if saveError != nil {
                println("Error saving record: \(saveError.localizedDescription)")
                } else {
                println("Successfully updated record!")
                }
            })
        }
    })

Case 2 - you know there is a record corresponding to your conditions, and you want to update it:

let predicate = yourPredicate // better be accurate to get only the record you need
var query = CKQuery(recordType: YourRecordType, predicate: predicate)
database.performQuery(query, inZoneWithID: nil, completionHandler: { (records, error) in
     if error != nil {
                println("Error querying records: \(error.localizedDescription)")                    
     } else {
         if records.count > 0 {
             let record = records.first as! CKRecord
             // Now you have grabbed your existing record from iCloud
             // Apply whatever changes you want
             record.setObject(aValue, forKey: attributeToChange)

             // Save this record again
             database.saveRecord(record, completionHandler: { (savedRecord, saveError in
                   if saveError != nil {
                     println("Error saving record: \(saveError.localizedDescription)")                
                   } else {
                     println("Successfully updated record!")
                   }
             })
         }
     }
})

Case 3 - you want to grab multiple records, and update them all at once:

let predicate = yourPredicate // can be NSPredicate(value: true) if you want them all
var query = CKQuery(recordType: YourRecordType, predicate: predicate)
database.performQuery(query, inZoneWithID: nil, completionHandler: { (records, error) in
     if error != nil {
                println("Error querying records: \(error.localizedDescription)")                    
     } else {
             // Now you have grabbed an array of CKRecord from iCloud
             // Apply whatever changes you want
             for record in records {                 
                 record.setObject(aValue, forKey: attributeToChange)
             }
             // Save all the records in one batch
             var saveOperation = CKModifyRecordsOperation(recordsToSave: records, recordIDsToDelete: nil)
             saveOperation.savePolicy = .IfServerRecordUnchanged // default
             saveOperation.modifyRecordsCompletionBlock = { savedRecords, deletedRecordsIDs, error in
                  if error != nil {
                      println("Error saving records: \(error.localizedDescription)")                            
                  } else {
                      println("Successfully updated all the records")
                  }
             }
             database.addOperation(saveOperation)
     }
})

Now, that was a lenghty answer to your question, but your code mixed both a unitary save method with a CKModifyRecordsOperation.

Also, you have to understand that, each time you create a CKRecord, CloudKit will give it a unique identifier (the record.recordID.recordName), unless you provide one yourself. So you have to know if you want to fetch an existing record, or create a new one before calling all these beautiful methods :-) If you try to create a new CKRecord using the same unique identifier as another one, then you'll most certainly get an error.

like image 22
Frédéric Adda Avatar answered Nov 06 '22 11:11

Frédéric Adda


I had the same error, but I was already fetching the record by ID as Guto described. It turned out I was updating the same record multiple times, and things were getting out of sync.

I have an update-and-save method that gets called by the main thread, sometimes rapidly.

I'm using blocks and saving right away, but if you're updating records quickly you can arrive in a situation where the following happens:

  1. Fetch record, get instance A'.
  2. Fetch record, get instance A''.
  3. Update A' and save.
  4. Update A'' and save.

Update of A'' will fail because the record has been updated on the server.

I fixed this by ensuring that I wait to update the record if I'm in the midst updating it.

like image 1
Chris Garrett Avatar answered Nov 06 '22 13:11

Chris Garrett