如何在创建新记录时使用 CloudKit 不延迟地更新 TableView 中的数据

How to update data in TableView without the delay using CloudKit when Creating new Records

我的应用程序中有 2 个视图控制器。

第一个 "MainViewController" 显示一个 tableView,其中包含从私有 CloudKit 数据库中获取的 CKRecords 字段。在此 VC 的 viewWillAppear 方法中,我从 CloudKit 获取记录并重新加载表视图的数据以显示用户先前保存在 CloudKit 中的最新获取结果。

第二个视图控制器 "CreateRecordViewController" 用于创建 CKRecords 并将它们保存到 CloudKit 的私有数据库。

所以我在 CreateRecordViewController 中创建记录并在 MainViewController 中显示它们。

问题如下:当我在 CreateRecordViewController 中创建记录时,它保存在 CloudKit 服务器上,但是在关闭此 CreateRecordViewController 并转到 MainViewController 之后,tableview 并不总是及时更新。

这是在我的 CreateRecordViewController 中保存记录的代码:

     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)")
                }
            }

保存记录后,我关闭 CreateRecordViewController 并查看 MainViewController。

正如我之前在 MainViewController 的 viewWillAppear 中所说,我检查 iCloud 是否可用,如果可用,我通过查询从 CloudKit 获取所有记录并将它们显示在 tableView 中。

 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() 中,我还在查询成功时异步重新加载 tableview 以显示最新结果。

我的 MainViewController 中的 loadRecordsFromiCloud 方法如下所示:

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 服务器工作得更快时,我可以在 tableView 中看到新记录,但大多数时候会有延迟(我在 tableview 中看不到新结果的那一刻MainViewController 加载)我认为这是因为当我获取记录时它获取旧数据(不知道为什么),但也许我又犯了一个错误。 这是一个糟糕的用户体验,我想知道如何避免这种延迟。我希望我的 tableView 在我关闭 CreateRecordViewController 后立即显示更新的结果。

我的第一个想法是订阅 CloudKit Records changes,并在收到通知时在 tableView 中获取和重新加载数据,但我真的不需要推送通知(我宁愿只有一个方法在我知道所有 CloudKit 记录已保存后或在我知道创建了新记录后,我可以从 CloudKit 获取数据的代码,在获取并获取 tableView 的数据后,我会调用 tableView.reloadData 例如),但我不确定如何(用什么方法)实现这个权利,也不确定这是否是最好的解决方案。我还听说在 CloudKit 的 WWDC 2016 视频中,现在有一些与订阅记录更改相关的新方法,也许其中一些方法可以提供帮助(不确定)。为这个问题(延迟问题)寻找最好的或任何好的和简单的解决方案。

我正在使用 XCode 8,iOS 10,swift 3

无法保证查询中记录何时可用,但您可以做一些事情。您可以将新记录拼接回去。因为当您创建和保存记录时,您拥有记录 ID,您可以进行 ckfetchrecords 操作并从新记录传递 ID,并且您可以保证立即取回它。索引有时需要一段时间,这让 CloudKit 感到沮丧。因此,基本上保证快速数据库的最佳方法是进行查询,如果新记录 ID 不在其中,则使用该 ID 进行提取并将其附加到结果中。希望这是有道理的。

我之前不得不这样做,因为我不太喜欢 CK。这是将记录拼接回操作的 link。https://developer.apple.com/reference/cloudkit/ckfetchrecordsoperation also if you are using images check out this library I made that allows you to exclude the image data keys and download and cache on demand that could speed up your queries. https://github.com/agibson73/AGCKImage

评论后编辑:

我认为您没有得到的部分是,由于索引的工作方式,记录可能会或可能不会随 viewcontroller 1 中的查询一起出现。您甚至在问题中提到它会获取旧数据。这是由于服务器索引。如果您删除记录,也会发生同样的情况。它可能仍会在查询中显示一段时间。在这种情况下,您会跟踪最近删除的记录 ID 并在查询后将其删除。同样,我所说的这种手动添加和删除是保证用户看到的内容和查询结果与用户期望的保持同步的唯一方法。

这是一些代码,虽然完全未经测试,但我希望它能帮助您形象地理解我上面所说的内容。

   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({[=10=].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({[=10=]}) + results!.flatMap({[=10=]})
                            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()
        }

    }
}

有点乱,但您可以看到我正在将丢失的 recordID(如果不是 nil)拼接回您正在执行的查询,因为不能保证该查询实时为您提供预期的新记录。在这种情况下,self.passedBackNewRecordID 是根据 Viewcontroller 中的新记录 ID 设置的 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()
}

这应该非常适合您的示例,并允许您测试我所说的内容,可能只需要很小的改动。