NSPersistentStoreRemoteChangeNotification 没有被解雇

NSPersistentStoreRemoteChangeNotification not getting fired

我正在尝试在使用 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 实施中的错误还是我遗漏了所需的任何设置?

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

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

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

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

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

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

注意最后一个参数 container.persistentStoreCoordinator

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

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

不知道是不是bug。只需下载并 运行 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)
}

我能够通过 iCloud 在项目中的两个设备之间可靠地回显核心数据更改。但是我到了需要访问更改历史记录的地步。 Apple 在 Consuming Relevant Store Changes

中很好地描述了设置步骤

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

If you will be configuring custom persistent store descriptions, you must set this property before calling loadPersistentStores(completionHandler:)

我正在配置 persistentStoreDescriptions inside of loadPersistentStores(completionHandler:) 所以最明显的方法是在 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")
}

斯威夫特UI

这是一种在 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 {
            [=11=].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
    }
}