【问题标题】:NSPersistentStoreRemoteChangeNotification not getting firedNSPersistentStoreRemoteChangeNotification 没有被解雇
【发布时间】:2020-03-28 05:20:24
【问题描述】:

我正在尝试在使用NSPersistentCloudKitContainer 的CoreData+CloudKit 项目中执行历史跟踪。我一直在关注苹果的sample project

我想在远程存储更新后执行某些任务。为此,苹果建议在应用程序的“签名和功能的后台模式”部分启用远程通知。

我已为我的项目启用历史跟踪,如 Apple 的示例项目所示。

    // turn on persistent history tracking
    let description = container.persistentStoreDescriptions.first
    description?.setOption(true as NSNumber,
                           forKey: NSPersistentHistoryTrackingKey)

    // ...

我还注册了我的商店以监听商店的变化。

    // turn on remote change notifications
    let remoteChangeKey = "NSPersistentStoreRemoteChangeNotificationOptionKey"
    description?.setOption(true as NSNumber,
                               forKey: remoteChangeKey)

    // ...

还添加了观察者来监听NSPersistentStoreRemoteChangeNotification

但是没有NSPersistentStoreRemoteChangeNotification 被解雇。为了确保我的实现没有错误,我只是在@objc func storeRemoteChange(_ notification: Notification) Apple 提供的示例代码中放置了断点,但我仍然看不到任何通知被触发并且没有激活断点。

我已经了解示例项目中对标签的重复数据删除,也尝试过对其进行测试,但没有任何成功。这是 Apple 实施中的错误还是我缺少任何所需的设置?

【问题讨论】:

标签: core-data cloudkit ios13 nspersistentcloudkitcontainer


【解决方案1】:

我猜你正在观察容器而不是商店协调员,像这样添加你的观察者:

    NotificationCenter.default.addObserver(
        self, selector: #selector(type(of: self).storeRemoteChange(_:)),
        name: .NSPersistentStoreRemoteChange, object: container.persistentStoreCoordinator)

注意最后一个参数container.persistentStoreCoordinator

还有一个警告,这个通知来自所有不同的线程,所以你要小心并发。只需在该方法中放置 5 秒睡眠,您就会在应用启动时看到 3 个不同的线程调用它。这可能就是为什么在示例中有一个 historyQueuemaxOperationCount 1 来处理它的原因。

有些通知在userInfo 中有NSPersistentHistoryTokenKey 不知道为什么。

【讨论】:

  • 是的,这也有效!我检查了它。不知道为什么苹果提供的最后一个参数不正确?
  • 我猜在编写样本时会为容器发送通知并且他们更改了它。我之前也看到过同样的事情,使用 UIViewControllerShowDetailTargetDidChangeNotification 的示例被破坏了,因为它们在 iOS 13 中将对象从 UISplitViewController 更改为 UISplitViewControllerPanelImpl。
  • 谢谢,@malhal!我还在我的“香草”应用程序中检查了这一点(在下面的回答中提到),它就像一个魅力。请问您是如何发现合适的观察对象的?
  • 我使用 object:nil 禁用过滤器,然后检查收到的 NSNotification 中的对象类型。
  • 我已经看到这个工作只是container 在示例代码中。我怀疑这在 iOS 14 中发生了变化,所以如果你仍然支持 iOS 13,你可能需要同时测试/分支
【解决方案2】:

调试OP提到的示例应用程序,我观察到以下内容:

  • 从 XCode 版本 11.3 (11C29) 开始,选项键 (NSPersistentStoreRemoteChangeNotificationPostOptionKey) 和通知名称 (.NSPersistentStoreRemoteChange) 都有 SDK 常量,这些都反映在最新下载的示例代码中。
  • 示例应用程序注册了错误对象上的远程更改通知,因此它永远不会收到任何通知。根据接受的答案更改发件人可以解决此问题。
  • 应用 UI 始终会更新以反映从云端收到的更改,但这些更新不是由远程更改通知提示,而是由应用的 NSFetchedResultsController 委托使用 controllerDidChangeContent 回调来刷新 UI。
  • 示例应用程序使用的标准NSPersistentCloudKitContainer 自动将所有云发送的更新导入到本地持久存储中,因为persistentStore 设置为历史跟踪,viewContext 设置为自动更新对于最新一代的数据,每次导入都会触发一次 UI 更新。

基于这些观察,我根据您通过指定使用 CoreData、CloudKit 和 SwiftUI 获得的 XCode 模板从头开始编写了一个小应用程序。我以与示例应用程序中设置相同的方式设置其持久容器和查看上下文,并使用 SwiftUI 的 @FetchRequest 包装器获取主视图显示中的数据。果然,我看到了完全相同的远程导入行为没有使用任何远程更改通知,并且每次导入后都会更新 UI。

然后我确认,根据接受的答案,如果我正确注册了远程更改通知,就会收到它们。它们似乎是在 NSPersistentCloudKit 中的每个接收和导入操作完成后发送的。不需要观察它们来获取由这些导入启动的本地数据更改的通知。

【讨论】:

  • 嗨,您最终找到解决方案了吗?我处于这种确切的情况,现在我只是依靠 automaticMergesChangesFromParent 让它为云+扩展更改做自己的事情。然后我在 willEnterForegroundNotification 上强制执行一个新的 fetchrequest。这是我能想到的唯一方法。这很糟糕吗?
  • 就我个人而言,只要本地数据得到更新,并且本地更新可以上传到云端,我就很高兴不必自己处理通知。所以不,在我看来,你的方法一点也不差。顺便说一句,我不确定您是否需要在进入前台时强制执行 fetch 请求:我看到所有更新都通过正常的 @ObservedObject 更新强制刷新 fetch 请求。
【解决方案3】:

我不知道这是否是一个错误。只需下载并运行 Apple 的示例项目,但永远不会触发 NSPersistentStoreRemoteChangeNotification

我在我的 AppDelegate 中为同一个 NSPersistentStoreRemoteChangeNotification 添加了一个观察者,它正在触发。

我在 AppDelegate 中添加了通知观察者,然后简单地调用 CoreDataStack 的StoreRemoteChange(_:)。此外,标签重复数据删除逻辑工作正常。

这是我在 AppDelegate 中添加的代码

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        // The view controller hierarchy is defined in the main storyboard.
        guard let splitViewController = window?.rootViewController as? UISplitViewController,
            let navController = splitViewController.viewControllers[splitViewController.viewControllers.count - 1] as? UINavigationController,
            let topViewController = navController.topViewController else {
                return false
        }
        // Configure the splitViewController.
        topViewController.navigationItem.leftBarButtonItem = splitViewController.displayModeButtonItem
        splitViewController.delegate = self
        splitViewController.preferredDisplayMode = .allVisible

        // Observe Core Data remote change notifications.
        NotificationCenter.default.addObserver(
            self, selector: #selector(type(of: self).storeRemoteChange(_:)),
            name: .NSPersistentStoreRemoteChange, object: nil)

        return true
    }

@objc
func storeRemoteChange(_ notification: Notification) {
        coreDataStack.storeRemoteChange(notification)
}

【讨论】:

    【解决方案4】:

    我能够通过 iCloud 在我的项目中的两个设备之间可靠地回显 Core Data 更改。但是我到了需要访问更改历史记录的地步。 Apple 在Consuming Relevant Store Changes 中很好地描述了设置它的步骤

    我跟着并愉快地将相关代码复制并粘贴到我的应用程序中。但是 NSPersistentStoreRemoteChange 通知没有通过。就像喜剧一样,时机就是一切。根据documentation for persistentStoreDescriptions

    如果您要配置自定义持久存储描述,您 必须在调用之前设置此属性 loadPersistentStores(completionHandler:)

    我在 inside 的 loadPersistentStores(completionHandler:) 中配置了 persistentStoreDescriptions,所以最明显的方法是在 AppDelegate 中设置以下代码。

    // MARK: - Core Data stack
    
    lazy var persistentContainer: NSPersistentCloudKitContainer = {
        /*
         The persistent container for the application. This implementation
         creates and returns a container, having loaded the store for the
         application to it. This property is optional since there are legitimate
         error conditions that could cause the creation of the store to fail.
        */
        let container = NSPersistentCloudKitContainer(name: "yourProjectNameGoesHere")
        
        // turn on persistent history tracking
        // https://developer.apple.com/documentation/coredata/consuming_relevant_store_changes
        let description = container.persistentStoreDescriptions.first
        description?.setOption(true as NSNumber,
                               forKey: NSPersistentHistoryTrackingKey)
        
        // turn on remote change notifications
        let remoteChangeKey = "NSPersistentStoreRemoteChangeNotificationOptionKey"
        description?.setOption(true as NSNumber,
                                   forKey: remoteChangeKey)
        
        // this will make background updates from iCloud available to the context.
        container.viewContext.automaticallyMergesChangesFromParent = true
        
        // call this LAST, after the persistentStoreDescriptions configuration.  
        container.loadPersistentStores(completionHandler: { (storeDescription, error) in
            if let error = error as NSError? {
                // Replace this implementation with code to handle the error appropriately.
                // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
                 
                fatalError("Unresolved error \(error), \(error.userInfo)")
            }
        })
        
        return container
    }()
    

    从您的视图控制器或模型中获取通知。

    init() {
        NotificationCenter.default.addObserver(self,
            selector: #selector(fetchChanges),
                name: .NSPersistentStoreRemoteChange,
              object: pc.persistentStoreCoordinator)
    }
    
    @objc func fetchChanges(note: Notification) {
        print("Just received a NSPersistentStoreRemoteChange notification")
    }
    

    【讨论】:

    • 谢谢!您的回答让我很明显,我在 .loadPersistentStores() 完成块中设置了远程更改通知选项,这已经太晚了。现在可以了!
    【解决方案5】:

    SwiftUI

    这是一种在 SwiftUI 视图中通知 CloudKit 远程更改的方法,例如,更新依赖于 @FetchRequest 的列表的内容——代码中未显示简单:

    struct MyView: View {
        @State var refresh = UUID()
        var didRemoteChange = NotificationCenter.default.publisher(for: .NSPersistentStoreRemoteChange).receive(on: RunLoop.main)
        var body: some View {
            List {
                // ...
            }
            .id(refresh)
            .onReceive(self.didRemoteChange) { _ in
                self.refresh = UUID()
            }
        }
    }
    

    注意:.receive(on: RunLoop.main) 是必要的,以避免从后台线程修改 UI,因为远程事件可能(并且将会)从后台线程触发。或者,也可以使用.receive(on: DispatchQueue.main)

    为此,NSPersistentCloudKitContainer 需要设置为在发生远程更改时触发事件:

    struct PersistenceController {
        static let shared = PersistenceController()
        let container: NSPersistentCloudKitContainer
        init(inMemory: Bool = false) {
            container = NSPersistentCloudKitContainer(name: "YourApp")
            if inMemory {
                container.persistentStoreDescriptions.first!.url = URL(fileURLWithPath: "/dev/null")
            }
            //
            // Generate notifications upon remote changes
            //
            container.persistentStoreDescriptions.forEach {
                $0.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.automaticallyMergesChangesFromParent = true
            container.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
        }
    }
    

    【讨论】:

    • 这对我有用,非常感谢!
    猜你喜欢
    • 2015-10-07
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-02-16
    • 2016-07-15
    • 2021-04-05
    相关资源
    最近更新 更多