Как обновить данные в TableView без задержки, используя CloudKit при создании новых записей
В моем приложении 2 вида контроллеров.
Первый "MainViewController" отображает tableView с полями CKRecords, извлеченными из частной базы данных CloudKit. Внутри метода viewWillAppear этого VC я выбираю записи из CloudKit и перезагружаю данные таблицы, чтобы показать последние полученные результаты, которые ранее были сохранены пользователем в CloudKit.
Второй контроллер представления "CreateRecordView Controller" предназначен для создания CKRecords и сохранения их в частной базе данных CloudKit.
Поэтому я создаю записи в CreateRecordView Controller и показываю их в MainViewController.
Проблема заключается в следующем: когда я создаю запись в CreateRecordView Controller, она сохраняется на сервере CloudKit, но после закрытия этого CreateRecordView Controller и перехода в MainViewController представление таблицы не всегда обновляется во времени.
Это код сохранения записи в моем CreateRecordView Controller:
CloudKitManager.sharedInstance.privateDatabase.save(myRecord) { (savedRecord, error) -> Void in
if error == nil {
print("successfully saved record code: \(savedRecord)")
}
else {
// Insert error handling
print("error Saving Data to iCloud: \(error.debugDescription)")
}
}
После сохранения записи я закрываю CreateRecordView Controller и вижу MainViewController.
Как я уже говорил ранее в viewWillAppear из MainViewController, я проверяю, доступен ли iCloud, и, если он доступен, я выбираю все записи с запросом из CloudKit и показываю их в виде таблицы.
override func viewWillAppear(_ animated: Bool) {
CKContainer.default().accountStatus { (accountStatus, error) in
switch accountStatus {
case .noAccount: print("CloudKitManager: no iCloud Alert")
case .available:
print("CloudKitManager: checkAccountStatus : iCloud Available")
self.loadRecordsFromiCloud()
case .restricted:
print("CloudKitManager: checkAccountStatus : iCloud restricted")
case .couldNotDetermine:
print("CloudKitManager: checkAccountStatus : Unable to determine iCloud status")
}
}
}
В loadRecordsFromiCloud() я также перезагружаю асинхронный просмотр таблицы при успешном выполнении запроса, чтобы показать последние результаты.
Мой метод loadRecordsFromiCloud, расположенный в моем MainViewController, выглядит следующим образом:
func loadRecordsFromiCloud() {
// Get a private Database
let privateDatabase = CloudKitManager.sharedInstance.privateDatabase
let predicate = NSPredicate(value: true)
let query = CKQuery(recordType: "MyRecords", predicate: predicate)
let operation = CKQueryOperation(query: query)
privateDatabase.perform(query, inZoneWith: nil) { (results, error) in
if ((error) != nil) {
// Error handling for failed fetch from public database
print("error loading : \(error)")
}
else {
// Display the fetched records
//print(results!)
self.tableViewDataArray = results!
DispatchQueue.main.async {
print("DispatchQueue.main.sync")
self.tableView.reloadData()
}
}
}
}
Иногда, когда серверы CloudKit работают быстрее, я вижу новые записи в табличном представлении, но большую часть времени возникает задержка (я не вижу новых результатов в табличном представлении в тот момент, когда загружается MainViewController). я выбираю записи, он выбирает старые данные (не знаю почему), но, возможно, я сделал еще одну ошибку. Это плохой пользовательский опыт, и я хотел бы знать, как избежать этой задержки. Я хочу, чтобы мой tableView показывал обновленные результаты сразу после закрытия CreateRecordView Controller.
Моя первая идея заключалась в том, чтобы подписаться на изменения CloudKit Records, а также получать и перезагружать данные в tableView при получении уведомления, но мне действительно не нужно push-уведомление (я бы предпочел иметь только метод в коде, из которого я мог бы получать данные из CloudKit после того, как я знаю, что все записи CloudKit сохранены, или после того, как я знаю, что создана новая запись, и после извлечения и получения данных для tableView я бы назвал tableView.reloadData, например), но я не уверен, как реализовать это право (в каком методе) и не уверен, что это лучшее решение. Я также слышал, что в видео WWDC 2016, посвященном CloudKit, теперь есть несколько новых методов, связанных с подпиской на изменения записи, возможно, некоторые из этих методов могут помочь (не уверен). Ищите лучшее или любое хорошее и простое решение для этой проблемы (проблема задержки).
Я использую XCode 8, iOS 10, Swift 3
1 ответ
Нет никаких гарантий относительно того, когда запись будет доступна в запросе, но есть что-то, что вы можете сделать. Вы можете сшить новую запись обратно. Потому что когда вы создаете и сохраняете запись, у вас есть идентификатор записи, вы можете сделать ckfetchrecordsoperation и передать идентификатор из новой записи, и вы гарантированно получите его немедленно. Индексация иногда может занять некоторое время, и это разочаровывает в CloudKit. Таким образом, по сути, лучший способ гарантировать быструю базу данных - это сделать запрос, и, если нового идентификатора записи нет, сделать выборку с идентификатором и добавить его к своим результатам. Надеюсь, это имеет смысл.
Я должен был сделать это раньше, и так как я не был слишком увлечен CK. Вот ссылка на операцию для сшивания записи обратно. https://developer.apple.com/reference/cloudkit/ckfetchrecordsoperation также, если вы используете изображения, посмотрите эту библиотеку, которую я сделал, что позволяет исключить ключи данных изображения и загружать и кэшировать по требованию, что может ускорить ваши запросы. https://github.com/agibson73/AGCKImage
Изменить после комментария:
Я думаю, что та часть, которую вы не получаете, является записью, которая может или не может соответствовать запросу в viewcontroller 1 из-за способа индексации. Вы даже упоминаете в своем вопросе, что он выбирает старые данные. Это связано с индексацией сервера. То же самое произойдет, если вы удалили запись. Это все еще может появиться в течение некоторого времени в запросе. В этом случае вы отслеживаете недавно удаленные идентификаторы записей и удаляете их после запроса. Опять же, это ручное добавление и удаление, о котором я говорю, - единственный способ гарантировать то, что увидят пользователи, и результаты запроса будут синхронизированы с тем, что ожидает пользователь.
Вот некоторый код, хотя и полностью непроверенный, который, я надеюсь, поможет вам визуализировать то, что я говорю выше.
func loadRecordsFromiCloud() {
// Get a private Database
let privateDatabase = CKContainer.default().privateCloudDatabase
let predicate = NSPredicate(value: true)
let query = CKQuery(recordType: "MyRecords", predicate: predicate)
privateDatabase.perform(query, inZoneWith: nil) { (results, error) in
if ((error) != nil) {
// Error handling for failed fetch from public database
print("error loading : \(error)")
}
else {
//check for a newRecord ID that might be missing from viewcontroller 2 that was passed back
if self.passedBackNewRecordID != nil{
let newResults = results?.filter({$0.recordID == self.passedBackNewRecordID})
//only excute if there is a new record that is missing from the query
if newResults?.count == 0{
//houston there is a problem
let additionalOperation = CKFetchRecordsOperation(recordIDs: [self.passedBackNewRecordID!])
additionalOperation.fetchRecordsCompletionBlock = { recordsDict,fetchError in
if let newRecords = recordsDict?.values as? [CKRecord]{
//stitch the missing record back in
let final = newRecords.flatMap({$0}) + results!.flatMap({$0})
self.reloadWithResults(results: final)
self.passedBackNewRecordID = nil
}else{
self.reloadWithResults(results: results)
self.passedBackNewRecordID = nil
}
}
privateDatabase.add(additionalOperation)
}else{
//the new record is already in the query result
self.reloadWithResults(results: results)
self.passedBackNewRecordID = nil
}
}else{
//no new records missing to do additional check on
self.reloadWithResults(results: results)
}
}
}
}
func reloadWithResults(results:[CKRecord]?){
self.tableViewDataArray = results!
DispatchQueue.main.async {
print("DispatchQueue.main.sync")
self.tableView.reloadData()
}
}
}
Это немного беспорядок, но вы можете видеть, что я вставляю отсутствующий идентификатор записи, если не ноль, обратно в запрос, который вы выполняете, потому что этот запрос не гарантируется в реальном времени, чтобы дать вам ожидаемые новые записи. В этом случае self.passedBackNewRecordID устанавливается на основе нового recordID из Viewcontroller 2. Как вы устанавливаете или отслеживаете эту переменную, зависит от вас, но вам, вероятно, нужна вся система очередей, потому что то, что я вам говорю, применяется для внесения изменений в запись как а также удаляет. Поэтому в производственном приложении мне нужно было отслеживать записи, в которых были изменения, удаления и добавления, и получать свежую версию каждого из них, чтобы вы могли представить сложность списка объектов. Поскольку я прекратил использовать CloudKit, потому что захоронение или индексирование занимает слишком много времени, чтобы показать изменения в запросах.
Чтобы проверить ваш сохраненный код может выглядеть следующим образом.
CloudKitManager.sharedInstance.privateDatabase.save(myRecord) { (savedRecord, error) -> Void in
if error == nil {
print("successfully saved record code: \(savedRecord)")
//save temporarily to defaults
let recordID = "someID"
UserDefaults.standard.set(recordID, forKey: "recentlySaved")
UserDefaults.standard.synchronize()
//now we can dismiss
}
else {
// Insert error handling
print("error Saving Data to iCloud: \(error.debugDescription)")
}
}
И в коде, где вы вызываете запрос в контроллере представления 1, возможно, viewWillAppear, вы можете вызвать это
func startQuery(){
UserDefaults.standard.synchronize()
if let savedID = UserDefaults.standard.value(forKey: "recentlySaved") as? String{
passedBackNewRecordID = CKRecordID(recordName: savedID)
//now we can remove from Userdefualts
UserDefaults.standard.removeObject(forKey: "recentlySaved")
UserDefaults.standard.synchronize()
}
self.loadRecordsFromiCloud()
}
Это должно очень хорошо соответствовать вашему примеру и позволить вам протестировать то, что я говорю, с небольшими изменениями.