Swift / CloudKit:记录更改后,上传触发“服务记录已更改”

Posted

技术标签:

【中文标题】Swift / CloudKit:记录更改后,上传触发“服务记录已更改”【英文标题】:Swift / CloudKit: After record changed, upload triggers "Service Record Changed" 【发布时间】:2016-08-25 23:18:03 【问题描述】:

我正在尝试将 CKReference 添加到云套件中的记录,但尝试不断触发“服务记录已更改”。从我的 println 显示的控制台消息(控制台消息和代码如下),我正在上传带有 0 个引用的记录,然后当我附加引用时,我看到尝试上传带有 1 个引用的记录。然后我得到了错误。

据我了解,不应触发“服务记录已更改”,因为参考列表中的值已更改(记录有一个完整的额外字段)。即使我在开发模式下,我手动为Reference List创建了key-value字段,因为在reference list为空时,第一条记录上传不包含该字段(上传空数组会导致另一个错误)。

我将在控制台消息之后按相关顺序包含代码(您将能够看到大部分 println)。整个项目都在 github 上,如果需要,我可以链接到它或包含更多代码。

相关控制台:

name was set
uploading TestCrewParticipant
with 0 references
if let projects
upload succeeded: TestCrewParticipant
attaching reference
adding TestVoyage:_d147aa657fbf2adda0c82bf30d0e29a9 from guard
references #: Optional(1)
uploading TestCrewParticipant
with 1 references
if let projects
success: 1
uploading TestCrewParticipant
with 1 references
if let projects
success: 1
local storage tested: TestCrewParticipant
u!error for TestCrewParticipant
CKError: <CKError 0x7fcbd05fa960: "Server Record Changed" (14/2004); server message = "record to insert already exists"; uuid = 96377029-341E-487C-85C3-E18ADE1119DF; container ID = "iCloud.com.lingotech.cloudVoyageDataModel">
u!error for TestCrewParticipant
CKError: <CKError 0x7fcbd05afb80: "Server Record Changed" (14/2004); server message = "record to insert already exists"; uuid = 3EEDE4EC-4BC1-4F18-9612-4E2C8A36C68F; container ID = "iCloud.com.lingotech.cloudVoyageDataModel">
passing the guard 

CrewParticipant 的代码:

/**
 * This array stores a conforming instance's CKReferences used as database
 * relationships. Instance is owned by each record that is referenced in the
 * array (supports multiple ownership)
 */
var references: [CKReference]  return associatedProjects ?? [CKReference]() 

// MARK: - Functions

/**
 * This method is used to store new ownership relationship in references array,
 * and to ensure that cloud data model reflects such changes. If necessary, ensures
 * that owned instance has only a single reference in its list of references.
 */
mutating func attachReference(reference: CKReference, database: CKDatabase) 
print("attaching reference")
    guard associatedProjects != nil else 
print("adding \(reference.recordID.recordName) from guard")
        associatedProjects = [reference]
        uploadToCloud(database)
        return
    
print("associatedProjects: \(associatedProjects?.count)")
    if !associatedProjects!.contains(reference) 
print("adding \(reference.recordID.recordName) regularly")
        associatedProjects!.append(reference)
        uploadToCloud(database)
    


/**
 * An identifier used to store and recover conforming instances record.
 */
var recordID: CKRecordID  return CKRecordID(recordName: identifier) 

/**
 * This computed property generates a conforming instance's CKRecord (a key-value
 * cloud database entry). Any values that conforming instance needs stored should be
 * added to the record before returning from getter, and conversely should recover
 * in the setter.
 */
var record: CKRecord 
    get 
        let record = CKRecord(recordType: CrewParticipant.REC_TYPE, recordID: recordID)

        if let id = cloudIdentity  record[CrewParticipant.TOKEN] = id 

// There are several other records that are dealt with successfully here.

print("if let projects")
        // Referable properties
        if let projects = associatedProjects 
print("success: \(projects.count)")
            record[CrewParticipant.REFERENCES] = projects
        

        return record
    

    set  matchFromRecord(newValue) 

上传发生的通用代码(适用于其他几个类):

/**
 * This method uploads any instance that conforms to recordable up to the cloud. Does not check any 
 * redundancies or for any constraints before (over)writing.
 */
func uploadRecordable<T: Recordable>
    (instanceConformingToRecordable: T, database: CKDatabase, completionHandler: (() -> ())? = nil) 
print("uploading \(instanceConformingToRecordable.recordID.recordName)")
if let referable = instanceConformingToRecordable as? Referable  print("with \(referable.references.count) references") 
    database.saveRecord(instanceConformingToRecordable.record)  record, error in
        guard error == nil else 
print("u!error for \(instanceConformingToRecordable.recordID.recordName)")
            self.tempHandler =  self.uploadRecordable(instanceConformingToRecordable,
                                                       database: database,
                                                       completionHandler: completionHandler) 
            CloudErrorHandling.handleError(error!, errorMethodSelector: #selector(self.runTempHandler))
            return
        
print("upload succeeded: \(record!.recordID.recordName)")
        if let handler = completionHandler  handler() 
    


/**
 * This method comprehensiviley handles any cloud errors that could occur while in operation.
 *
 * error: NSError, not optional to force check for nil / check for success before calling method.
 *
 * errorMethodSelector: Selector that points to the func calling method in case a retry attempt is
 * warranted. If left nil, no retries will be attempted, regardless of error type.
 */
static func handleError(error: NSError, errorMethodSelector: Selector? = nil) 

    if let code: CKErrorCode = CKErrorCode(rawValue: error.code) 
        switch code 

        // This case requires a message to USER (with USER action to resolve), and retry attempt.
        case .NotAuthenticated:
            dealWithAuthenticationError(error, errorMethodSelector: errorMethodSelector)

        // These cases require retry attempts, but without error messages or USER actions.
        case .NetworkUnavailable, .NetworkFailure, .ServiceUnavailable, .RequestRateLimited, .ZoneBusy, .ResultsTruncated:
            guard errorMethodSelector != nil else  print("Error Retry CANCELED: no selector"); return 
            retryAfterError(error, selector: errorMethodSelector!)

        // These cases require no message to USER or retry attempts.
        default:
            print("CKError: \(error)")
                    
    

【问题讨论】:

【参考方案1】:

看起来您每次保存时都在创建一个新的 CKRecord。

CloudKit 返回ServerRecordChanged 告诉您服务器上已存在具有相同recordID 的记录,并且您的保存尝试被拒绝,因为服务器记录的版本不同。

每条记录都有一个更改标签,允许服务器跟踪该记录的保存时间。当您保存记录时,CloudKit 会将记录的本地副本中的更改标记与服务器上的更改标记进行比较。如果两个标签不匹配——意味着存在潜在的冲突——服务器使用 [CKModifyRecordsOperation 的 savePolicy 属性] 中的值来确定如何继续。

来源:CKModifyRecordsOperation Reference

虽然您使用的是CKDatabase.saveRecord 便捷方法,但这仍然适用。默认的 savePolicy 是ifServerRecordUnchanged

首先,我建议转换到 CKModifyRecordsOperation,尤其是在您保存多条记录的情况下。它使您可以更好地控制流程。

其次,在保存对现有记录的更改时,您需要从服务器对 CKRecord 进行更改。您可以通过以下任何方式完成此操作:

    从 CloudKit 请求 CKRecord,对该 CKRecord 进行更改,然后将其保存回 CloudKit。 使用 CKRecord Reference 中的建议存储已保存的 CKRecord(保存后在完成块中返回的),持久保存此数据,然后将其取消归档以获取 CKRecord,您可以修改并保存到服务器。 (这避免了一些网络往返来请求服务器 CKRecord。)

在本地存储记录

如果您将记录存储在本地数据库中,请使用 encodeSystemFields(with:) 方法对记录的元数据进行编码和存储。元数据包含记录 ID 和更改标记,稍后需要将本地数据库中的记录与 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)

来源:CloudKit Tips and Tricks - WWDC 2015

请记住:如果另一台设备在您请求/上次保存并存储服务器记录后保存对记录的更改,您仍然会遇到 ServerRecordChanged 错误。您需要通过获取最新的服务器记录并将更改重新应用到该 CKRecord 来处理此错误。

【讨论】:

非常感谢您的回复。我知道我错过了一些重要的东西......我需要对我的代码进行大量更改! 这还是值得的。当您这样做时,我建议调查所有其他可能的 CloudKit 错误代码,并考虑如何处理它们。一个强大的 CloudKit 应用程序应该适当地处理所有可能的错误(在某些情况下,如何处理可能取决于您的应用程序)。 再次感谢@breakobstacles,您看到我用于错误处理的方法了吗?除了“未验证”之外,我一直将大多数错误视为“使用计时器重新尝试”或“无法重试”。您是说我应该扩展我的 switch 语句还是应该完全放弃通用响应?我在处理云套件的错误处理方面遇到了很多麻烦,因此非常感谢任何进一步的建议、示例、资源等。 刚刚看完 2014 年 wwdc 视频并进入 2015 年……非常有帮助。再次感谢您的链接。【参考方案2】:

您可以使用 CKModifyRecordsOperation 的 savePolicy 绕过跟踪更改标签

modifyRecordsOperation.savePolicy = .allKeys

【讨论】:

以上是关于Swift / CloudKit:记录更改后,上传触发“服务记录已更改”的主要内容,如果未能解决你的问题,请参考以下文章

Swift CloudKit CoreData 远程更改通知也会触发本地更改

swift Swift - CloudKit - 保存记录

swift Swift - CloudKit - 获取多个记录

swift Swift - CloudKit:保存单个记录

swift Swift - CloudKit:获取记录

删除 CloudKit 记录 Swift 4