7

クラウド キットのレコードに CKReference を追加しようとしていますが、この試行によって "Service Record Changed" がトリガーされ続けます。私のprintlnが示したコンソールメッセージ(コンソールメッセージと以下のコード)から、参照が0のレコードをアップロードしています。次に、参照を添付すると、参照が1のレコードをアップロードしようとしています。次に、エラーが発生します。

私が理解しているように、参照リストの値が変更されたため、「サービスレコードが変更されました」はトリガーされるべきではありません (レコードには余分なフィールド全体があります)。私は開発モードですが、参照リストが空の場合、最初のレコードのアップロードにはフィールドが含まれていないため、参照リストのキー値フィールドを手動で作成しました (空の配列をアップロードすると別のエラーが発生します)。

コンソール メッセージの後に、関連性の高い順にコードを含めます (ほとんどの 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)")
        }            
    }
}
4

2 に答える 2

16

保存するたびに新しい CKRecord を作成しているようです。

CloudKit はServerRecordChanged、同じレコード ID を持つレコードがサーバー上に既に存在し、サーバー レコードのバージョンが異なっていたため、保存の試みが拒否されたことを通知するために戻ってきました。

各レコードには、そのレコードがいつ保存されたかをサーバーが追跡できるようにする変更タグがあります。レコードを保存すると、CloudKit はレコードのローカル コピーの変更タグとサーバー上の変更タグを比較します。2 つのタグが一致しない場合 (競合の可能性があることを意味します)、サーバーは [ CKModifyRecordsOperation の savePolicy プロパティ] の値を使用して処理方法を決定します。

ソース: CKModifyRecordsOperation リファレンス

便利な方法を使用していますがCKDatabase.saveRecord、これは引き続き適用されます。デフォルトの savePolicy は ですifServerRecordUnchanged

まず、特に複数のレコードを保存している場合は、CKModifyRecordsOperationに移行することをお勧めします。これにより、プロセスをより詳細に制御できます。

次に、既存のレコードに変更を保存するときに、サーバーから CKRecord に変更を加える必要があります。これは、次のいずれかで実現できます。

  1. CloudKit から CKRecord を要求し、その CKRecord に変更を加えてから、CloudKit に保存し直します。
  2. 保存された CKRecord (保存後に完了ブロックで返されたもの) をCKRecord Referenceのアドバイスに従って保存し、このデータを永続化してからアーカイブを解除して、変更してサーバーに保存できる CKRecord を取得します。(これにより、サーバー CKRecord を要求するためのネットワーク ラウンドトリップが回避されます。)

レコードをローカルに保存する

レコードをローカル データベースに保存する場合は、encodeSystemFields(with:) メソッドを使用して、レコードのメタデータをエンコードして保存します。メタデータには、ローカル データベース内のレコードを CloudKit によって保存されたレコードと同期するために後で必要になるレコード ID と変更タグが含まれています。

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 に再適用することで、このエラーを処理する必要があります。

于 2016-08-27T16:34:11.633 に答える