Swift CloudKit SaveRecord “Error saving record”

后端 未结 3 1167
有刺的猬
有刺的猬 2021-01-30 18:52

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.

相关标签:
3条回答
  • 2021-01-30 19:29

    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.

    0 讨论(0)
  • 2021-01-30 19:33

    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.

    0 讨论(0)
  • 2021-01-30 19:36

    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.

    0 讨论(0)
提交回复
热议问题