Swift CloudKit SaveRecord "Ошибка сохранения записи"

Я пытаюсь сохранить запись в 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. К сожалению, в документации Apple нет примеров. Я скучаю по этому (что вы получаете на MSDN).

Спасибо за внимание!

Ответ 1

Запись может быть сохранена в iCloud с использованием метода удобства CKDatabase saveRecord: или через CKModifyRecordsOperation. Если это одна запись, вы можете использовать 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
        } 
}

Приведенный выше код только направленно корректен, но не будет работать так, потому что к моменту, когда будет возвращен завершениеHandler fetchRecordWithID, saveRecord уже будет запущен. Простым решением было бы вставить saveRecord в completeHandler из fetchRecordWithID. Вероятно, лучшим решением было бы обернуть каждый вызов в 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 the case
}

saveRecordsOperation.modifyRecordsCompletionBlock { savedRecords, deletedRecordIDs, error in
    // deal with conflicts
    // set completionHandler of wrapper operation if it the case
}

database.addOperation(saveRecordsOperation)

Пока мало демонстрационного приложения CloudKitAtlas, которое находится в Objective-C. Надеюсь, это поможет.

Ответ 2

Вообще говоря, у вас есть унитарные методы (например, saveRecord), которые обрабатывают только одну запись за раз, а массовые операции (например, CKModifyRecordsOperation), которые касаются несколько записей одновременно.

Эти операции сохранения могут использоваться для сохранения записей или для обновления записей (т.е. для их извлечения, применения изменений к ним и последующего сохранения их).


Примеры SAVE:

Вы создаете запись и хотите сохранить ее в CloudKit DB:

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)

Примеры UPDATE:

Обычно у вас есть 3 случая, когда вы хотите обновлять записи:

  • вы знаете идентификатор записи (обычно это recordID.recordName записи, которую вы хотите сохранить: в этом случае, вы будете использовать методы fetchRecordWithID, а затем сохранитьRecord
  • вы знаете, что есть уникальная запись для обновления, но вы не знаете ее recordID: в этом случае вы будете использовать запрос с методом выполнитьQuery, выберите (только) тот, который вам нужен, и снова saveRecord

  • вы имеете дело со многими записями, которые вы хотите обновить: в этом случае вы будете использовать запрос для их получения (выполнитьQuery) и 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, используя тот же уникальный идентификатор, что и другой, вы наверняка получите сообщение об ошибке.

Ответ 3

У меня была такая же ошибка, но я уже собирал запись по идентификатору, описанную Гуто. Оказалось, что я несколько раз обновлял одну и ту же запись, и ситуация выходила из строя.

У меня есть метод update-and-save, который вызывается главным потоком, иногда быстро.

Я использую блоки и сохраняю сразу, но если вы быстро обновляете записи, вы можете прийти в ситуацию, когда происходит следующее:

  • Записать запись, получить экземпляр A '.
  • Получить запись, получить экземпляр A ''.
  • Обновить A 'и сохранить.
  • Обновить A 'и сохранить.

Обновление A '' завершится неудачно, потому что запись была обновлена ​​на сервере.

Я исправил это, убедившись, что я жду, чтобы обновить запись, если я нахожусь в середине обновления.