【问题标题】:How to fetch CloudKit data manually to update the UI using NSPersistentCloudKitContainer and SwiftUI?如何使用 NSPersistentCloudKitContainer 和 SwiftUI 手动获取 CloudKit 数据以更新 UI?
【发布时间】:2020-06-07 10:47:01
【问题描述】:

假设我们有一个工作的 NSPersistentCloudKitContainer 和一个名为 Item 的 CoreData 实体。假设我们要在 iOS、iPad 和 Mac 应用程序之间同步。

我看了这个WWDC session about syncing CoreData and CloudKit 并在我的 SwiftUI 应用程序中实现了基本同步。它可以正常工作,数据会传输到所有 3 台设备并自动同步。

但是在 Mac 上,如果不重新加载视图,数据不会出现(以及在模拟器中(因为它们没有收到推送通知)。它来了,但没有自动刷新。我也测试过模拟器到真实设备,有时它会自动同步,有时我需要重新打开应用程序才能看到变化。

我很好奇是否有可以与 NSPersistentCloudKitContainer 一起使用的强制获取方法。或者也许有人知道在将 NSPersistentCloudKitContainer 与 SwiftUI 一起使用时手动重新获取数据的解决方法?

我还想在新数据开始出现时显示活动指示器,但不确定在哪里可以找到代码中的此开始获取点?

我使用了 Apple 提供的 UIKit sample code 并将其调整为在 SwiftUI 中工作。我还阅读了所有文档here

这是我使用的persistentContainer代码:

lazy var persistentContainer: NSPersistentCloudKitContainer = {

        let container = NSPersistentCloudKitContainer(name: "AppName")

        container.persistentStoreDescriptions.first?.setOption(true as NSNumber, forKey: NSPersistentStoreRemoteChangeNotificationPostOptionKey)

        container.loadPersistentStores(completionHandler: { (storeDescription, error) in
            if let error = error as NSError? {

                fatalError("Unresolved error \(error), \(error.userInfo)")
            }
        })

        container.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
        container.viewContext.automaticallyMergesChangesFromParent = true

        return container
    }()

如果你看到了,我没有在上面的代码中包含 container.viewContext.transactionAuthor = appTransactionAuthorNamesetQueryGenerationFrom(.current)NotificationCenter.default.addObserver。但我也尝试过他们并得到相同的结果。不确定我是否需要使用它们,因为在没有这些调用的情况下也可以进行同步。

在我的 Item 类中,我添加了这个函数来获取项目:

   static func getAllItems() -> NSFetchRequest<Item> {

        let request: NSFetchRequest<Item> = Item.fetchRequest() as! NSFetchRequest<Item>

        let sortDescriptor = NSSortDescriptor(key: "name", ascending: false)

        request.sortDescriptors = [sortDescriptor]

        return request

    }

在我的 ContentView 中,我有这个 MasterView 视图:

    struct MasterView: View {

    @FetchRequest(fetchRequest: Item.getAllItems()) var items: FetchedResults<Item>

    @Environment(\.managedObjectContext) var viewContext

    var body: some View {
        List {
            ForEach(items, id: \.self) { item in
                NavigationLink(
                    destination: DetailView(item: item)
                ) {
                    Text("\(item.name ?? "")")
                }
            }.onDelete { indices in
                self.items.delete(at: indices, from: self.viewContext)
            }
        }
    }
}

附: 我还注意到同步工作缓慢(也许这只是我的情况,不确定)。在与此代码同步之间,我必须等待 5 到 10 秒。也许有人知道这个等待时间是否正常?

【问题讨论】:

  • 嗯,我认为值得处理.NSPersistentStoreRemoteChange 通知并强制刷新 SwiftUI 列表,因为上下文通常在后台队列中静默合并,并且 UI 不会自动处理此问题。以下主题可能会有所帮助[当相关实体在 SwiftUI 中发生更改时,如何更新 @FetchRequest? ](stackoverflow.com/a/58777603/12299030) 和SwiftUI: List does not update automatically after deleting all Core Data Entity entries。注意将 UI 刷新从通知处理程序重定向到主队列。
  • @Adelmaer 您找到解决方案了吗?我也面临同样的问题。
  • 嗨@MikeBernardo,还没有,我认为它应该以这种方式工作。在真实设备上测试时效果更好。

标签: ios core-data swiftui cloudkit nspersistentcloudkitcontainer


【解决方案1】:

因为从远程同步的数据直接写入持久化,viewContext 不知道它们。但是我们可以通过 NSPersistentStoreRemoteChangeNotification 来通知我们。 fetchHistory,按作者过滤,我们得到同步的数据,然后合并到viewContext中,所以UI刷新。

  1. 设置。
        publicDescription.setOption(true as NSNumber, forKey: NSPersistentHistoryTrackingKey)
        publicDescription.setOption(true as NSNumber, forKey: NSPersistentStoreRemoteChangeNotificationPostOptionKey)
        ...
        context.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
        context.automaticallyMergesChangesFromParent = true
        context.transactionAuthor = containerName
        try? context.setQueryGenerationFrom(.current)
  1. 通知。
        NotificationCenter.default
            .publisher(for: .NSPersistentStoreRemoteChange)
//            .receive(on: DispatchQueue.main)
            .sink { [weak self] notification in
                guard let self = self else { return }
                self.processRemoteStoreChange(notification)
            }
            .store(in: &subscriptions)
  1. 获取历史记录并将其合并到 viewContext。
    private func processRemoteStoreChange(_ notification: Notification) {
        DispatchQueue(label: "history").async { [weak self] in
            guard let self = self else { return }
            let backgroundContext = self.container.newBackgroundContext()
            backgroundContext.performAndWait { [weak self] in
                guard let self = self else { return }
                
                let request = NSPersistentHistoryChangeRequest.fetchHistory(after: self.historyTimestamp)
                
                if let historyFetchRequest = NSPersistentHistoryTransaction.fetchRequest {
                    historyFetchRequest.predicate = NSPredicate(format: "author != %@", self.containerName)
                    request.fetchRequest = historyFetchRequest
                }
                
                guard let result = try? backgroundContext.execute(request) as? NSPersistentHistoryResult,
                      let transactions = result.result as? [NSPersistentHistoryTransaction],
                      transactions.isEmpty == false else {
                    return
                }
                
                foolPrint("transactions = \(transactions)")
                self.mergeChanges(from: transactions)
                
                if let timestamp = transactions.last?.timestamp {
                    DispatchQueue.main.async { [weak self] in
                        guard let self = self else { return }
                        self.historyTimestamp = timestamp
                    }
                }
            }
        }
    }
    
    private func mergeChanges(from transactions: [NSPersistentHistoryTransaction]) {
        context.perform {
            transactions.forEach { [weak self] transaction in
                guard let self = self, let userInfo = transaction.objectIDNotification().userInfo else { return }
                NSManagedObjectContext.mergeChanges(fromRemoteContextSave: userInfo, into: [self.context])
//                foolPrint("mergeChanges, \(transaction.timestamp): \(userInfo)")
            }
        }
    }

【讨论】:

    【解决方案2】:

    我在构建应用程序时遇到了类似的问题,因此我构建并发布了 ⛅️ CombineCloudKit 包。使用 Combine Publishers,可以轻松构建响应 CloudKit 操作的反应式 UI。

    https://github.com/chris-araman/CombineCloudKit

    我希望人们觉得它有用!

    【讨论】:

      猜你喜欢
      • 2021-02-23
      • 2021-05-11
      • 2021-02-07
      • 2020-03-27
      • 2020-12-20
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多