Swift CloudKit 保存记录出错:"错误保存记录"。

11

我正在尝试将一条记录保存到 CloudKit 中,但是出现了错误。我在其他地方看到过,这是一个需要知道如何保存的问题,但我无法让它正常工作。

    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
        }
    })

这让我报错:

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">!

不确定,因为我已经添加了CKModifyRecordsOperation,可惜苹果文档中没有示例。我很怀念MSDN上的那个(提供示例代码的)。

谢谢大家!

3个回答

25

您可以使用CKDatabase的便捷方法saveRecord:或通过CKModifyRecordsOperation将记录保存到iCloud。如果只有单个记录,您可以使用saveRecord:,但需要先使用fetchRecordWithID:获取要修改的记录,然后将其保存回iCloud中。否则,它只允许您保存具有新RecordID的记录。更多信息。

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
        } 
}

以上代码只是方向性正确,但不会按原样运行,因为在fetchRecordWithID的completionHandler返回时,saveRecord已经被触发了。一个简单的解决方法是将saveRecord嵌套在fetchRecordWithID的completionHandler中。可能更好的解决方法是将每个调用包装在NSBlockOperation中,并将它们添加到一个NSOperationQueue中,其中saveOperation依赖于fetchOperation。

如果您只更新单个记录,则您的代码的这部分将用于CKModifyRecordsOperation,并且不需要。

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

如果你使用CKModifyRecordsOperation,你还需要设置至少一个完成块并在检测到与现有记录发生冲突时处理错误:

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)

除了Objective-C编写的CloudKitAtlas演示应用程序之外,目前还没有太多的示例代码可供使用。希望这可以帮到你。


这个答案真的帮了我很多,但我遇到了一个障碍,就是编译时出现了错误,因为.perRecordCompletionBlock和.modifyRecordsCompletionBlock似乎需要在{}前面加上=,所以写成"saveRecordsOperation.perRecordCompletionBlock = {}"。 - William Robinson
你尝试过在保存之前设置适当的CKRecord的recordChangeTag吗?这样你就可以避免调用fetchRecordWithID,从而减少周转时间。 - János

23

通常情况下,您有像saveRecord这样只处理一个记录的 单个操作 方法,以及像CKModifyRecordsOperation这样处理多个记录的 批量操作 方法。

这些保存操作可用于保存记录,也可用于更新记录(即获取它们、对它们进行更改,然后再次保存它们)。


保存示例:

您创建了一条记录并希望将其保存到 CloudKit 数据库:

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!")
  }
})

您创建了一堆记录并希望一次性保存它们:

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)

更新示例:

通常,您有三种情况需要更新记录:

  1. 您知道要保存的记录的标识符(通常是要保存的记录ID.recordName):在这种情况下, 您将使用方法fetchRecordWithID,然后使用saveRecord方法
  2. 您知道有一个唯一的记录需要更新,但您不知道其记录ID:在这种情况下,您将使用带有方法的查询 performQuery,选择您需要的(唯一)记录,然后再次saveRecord

  3. 您正在处理需要更新的多个记录:在这种情况下,您将使用查询来获取所有记录 (performQuery),并使用CKModifyRecordsOperation来保存它们。

情况 1 - 您知道要更新的记录的唯一标识符:

    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!")
                }
            })
        }
    })

情况2 - 您知道存在与您条件相对应的记录,并且您想要更新它:

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!")
                   }
             })
         }
     }
})

第3种情况 - 您想一次性获取多条记录并将它们全部更新:

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)
     }
})

这是对你问题的详细回答,但是你的代码将单元保存方法和CKModifyRecordsOperation混合在一起了。

此外,你必须要知道每次创建CKRecord时,CloudKit都会为其分配一个唯一标识符(record.recordID.recordName),除非你自己提供一个。因此,在调用所有这些优秀的方法之前,你必须知道是想要获取现有记录还是创建新记录:-) 如果尝试使用与另一个记录相同的唯一标识符创建新的CKRecord,则很可能会出现错误。



非常感谢您的示例,它们确实帮助我掌握了它的要领! - Henk-Martijn
很高兴能帮到你 :-) - Frederic Adda
很棒的答案,帮助我理解了CloudKit操作的工作原理。 - ShayanK
很棒的答案,我认为这应该是被采纳的答案。 - user1105951

1
我遇到了同样的错误,但是我已经像Guto描述的那样通过ID获取记录。结果发现我正在多次更新同一条记录,而且事情变得不同步。
我有一个更新和保存方法,由主线程调用,有时会很快地调用。
我正在使用块并立即保存,但如果您快速更新记录,则可能出现以下情况:
1.获取记录,获取实例A'。 2.获取记录,获取实例A''。 3.更新A'并保存。 4.更新A''并保存。
A''的更新将失败,因为该记录已在服务器上更新。
我通过确保在更新记录时等待来修复此问题,如果我正在更新它,则等待更新记录。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接