Swift / CloudKit: после изменения записи загрузите триггеры "Service Record Changed"

Я пытаюсь добавить CKReference к записи в облачном наборе, но попытка продолжает вызывать "Service Record Changed". Из сообщений консоли, которые показала моя println (сообщения консоли и код ниже), я загружаю указанную запись с 0 ссылками, затем, когда я прикрепляю ссылку, я вижу попытку загрузить запись с 1 ссылкой. Тогда я получаю ошибку.

Насколько я понимаю, "Service Record Changed" не должен запускаться, потому что значения в списке ссылок изменились (запись имеет целое дополнительное поле). Несмотря на то, что я нахожусь в режиме разработки, я вручную создал поле значения ключа для Списка ссылок, потому что первая загрузка записи не включает поле, когда список ссылок пуст (загрузка пустого массива вызывает другую ошибку).

Я добавлю код в порядке релевантности (вы сможете увидеть большую часть 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 

Код от участника экипажа:

/**
 * 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 чтобы сообщить вам, что на сервере уже существует запись с тем же идентификатором записи, и ваша попытка сохранения была отклонена, поскольку версия записи сервера отличалась.

Каждая запись имеет тег изменения, который позволяет серверу отслеживать, когда эта запись была сохранена. Когда вы сохраняете запись, CloudKit сравнивает тег изменения в вашей локальной копии записи с тегом на сервере. Если два тега не совпадают - это означает, что существует потенциальный конфликт - сервер использует значение в свойстве [ savePolicy свойства CKModifyRecordsOperation ], чтобы определить, как действовать дальше.

Источник: CKModifyRecordsOperation Ссылка

Хотя вы используете CKDatabase.saveRecord метод удобства, это все еще применяется. По умолчанию savePolicy это ifServerRecordUnchanged,

Во-первых, я бы предложил перейти к CKModifyRecordsOperation, особенно если вы сохраняете несколько записей. Это дает вам гораздо больше контроля над процессом.

Во-вторых, вам нужно внести изменения в CKRecord с сервера при сохранении изменений в существующую запись. Вы можете сделать это одним из следующих способов:

  1. Запрос CKRecord из CloudKit, внесение изменений в этот CKRecord, а затем сохранение его обратно в CloudKit.
  2. Сохранение сохраненного CKRecord (возвращенного в блоке завершения после сохранения), используя рекомендации из Справочника CKRecord, сохраняя эти данные, а затем разархивируя его, чтобы получить обратно CKRecord, который вы можете изменить и сохранить на сервере. (Это позволяет избежать некоторых сетевых обращений к серверу CKRecord.)

Хранение записей локально

Если вы храните записи в локальной базе данных, используйте метод encodeSystemFields(with:) для кодирования и хранения метаданных записи. Метаданные содержат идентификатор записи и тег изменения, которые понадобятся позже для синхронизации записей в локальной базе данных с записями, хранящимися в 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 Советы и хитрости - WWDC 2015

Имейте в виду: вы все еще можете столкнуться с ServerRecordChanged ошибка, если другое устройство сохраняет изменения в записи после того, как вы ее запросили / в последний раз сохранили и сохранили запись на сервере. Вам нужно обработать эту ошибку, получив последнюю серверную запись и повторно применив свои изменения к этому CKRecord.

Вы можете использовать savePolicy CKModifyRecordsOperation для обхода отслеживания тега изменения

modifyRecordsOperation.savePolicy = .allKeys

Другие вопросы по тегам