Swift / CloudKit: 记录更改后,上传触发 "Service Record Changed"
Swift / CloudKit: After record changed, upload triggers "Service Record Changed"
我正在尝试将 CKReference 添加到云工具包中的记录,但尝试不断触发 "Service Record Changed"。 从我的 println 显示的控制台消息中(控制台消息和下面的代码),我正在上传带有 0 个引用的记录,然后当我附加引用时,我看到尝试上传带有 1 个引用的记录。然后我收到错误。
据我了解,"Service Record Changed"不应触发,因为参考列表中的值已更改(记录有一个完整的额外字段)。 即使虽然我处于开发模式,但我为参考列表手动创建了键值字段,因为当参考列表为空时,第一条记录上传不包括该字段(上传空数组会导致另一个错误)。
我将在控制台消息之后按相关性顺序包含代码(您将能够看到大部分 println)。整个项目都在 github 上,我可以 link 或在需要时包含更多代码。
相关控制台:
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)")
}
}
}
看起来你每次保存都在创建一个新的 CKRecord。
CloudKit 返回 ServerRecordChanged
告诉您服务器上已存在具有相同 recordID 的记录,并且您尝试保存被拒绝,因为服务器记录的版本不同。
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.
来源:CKModifyRecordsOperation Reference
虽然您使用的是 CKDatabase.saveRecord
便捷方法,但这仍然适用。默认的 savePolicy 是 ifServerRecordUnchanged
.
首先,我建议过渡到 CKModifyRecordsOperation,尤其是在保存多条记录时。它使您可以更好地控制流程。
其次,在保存对现有记录的更改时,您需要从服务器对 CKRecord 进行更改。您可以通过以下任一方式完成此操作:
- 从 CloudKit 请求 CKRecord,对该 CKRecord 进行更改,然后将其保存回 CloudKit。
- 使用 CKRecord Reference 中的建议存储已保存的 CKRecord(保存后在完成块中返回的那个),持久化此数据,然后将其解压缩到取回您可以修改并保存到服务器的 CKRecord。 (这避免了一些请求服务器 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)
来源:CloudKit Tips and Tricks - WWDC 2015
请记住:如果另一台设备在您请求/上次保存并存储服务器记录后保存了对记录的更改,您仍然会遇到 ServerRecordChanged
错误。您需要通过获取最新的服务器记录并将您的更改重新应用到该 CKRecord 来处理此错误。
您可以使用 CKModifyRecordsOperation 的 savePolicy 绕过跟踪更改标签
modifyRecordsOperation.savePolicy = .allKeys
我正在尝试将 CKReference 添加到云工具包中的记录,但尝试不断触发 "Service Record Changed"。 从我的 println 显示的控制台消息中(控制台消息和下面的代码),我正在上传带有 0 个引用的记录,然后当我附加引用时,我看到尝试上传带有 1 个引用的记录。然后我收到错误。
据我了解,"Service Record Changed"不应触发,因为参考列表中的值已更改(记录有一个完整的额外字段)。 即使虽然我处于开发模式,但我为参考列表手动创建了键值字段,因为当参考列表为空时,第一条记录上传不包括该字段(上传空数组会导致另一个错误)。
我将在控制台消息之后按相关性顺序包含代码(您将能够看到大部分 println)。整个项目都在 github 上,我可以 link 或在需要时包含更多代码。
相关控制台:
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)")
}
}
}
看起来你每次保存都在创建一个新的 CKRecord。
CloudKit 返回 ServerRecordChanged
告诉您服务器上已存在具有相同 recordID 的记录,并且您尝试保存被拒绝,因为服务器记录的版本不同。
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.
来源:CKModifyRecordsOperation Reference
虽然您使用的是 CKDatabase.saveRecord
便捷方法,但这仍然适用。默认的 savePolicy 是 ifServerRecordUnchanged
.
首先,我建议过渡到 CKModifyRecordsOperation,尤其是在保存多条记录时。它使您可以更好地控制流程。
其次,在保存对现有记录的更改时,您需要从服务器对 CKRecord 进行更改。您可以通过以下任一方式完成此操作:
- 从 CloudKit 请求 CKRecord,对该 CKRecord 进行更改,然后将其保存回 CloudKit。
- 使用 CKRecord Reference 中的建议存储已保存的 CKRecord(保存后在完成块中返回的那个),持久化此数据,然后将其解压缩到取回您可以修改并保存到服务器的 CKRecord。 (这避免了一些请求服务器 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)
来源:CloudKit Tips and Tricks - WWDC 2015
请记住:如果另一台设备在您请求/上次保存并存储服务器记录后保存了对记录的更改,您仍然会遇到 ServerRecordChanged
错误。您需要通过获取最新的服务器记录并将您的更改重新应用到该 CKRecord 来处理此错误。
您可以使用 CKModifyRecordsOperation 的 savePolicy 绕过跟踪更改标签
modifyRecordsOperation.savePolicy = .allKeys