NSPersistentStoreRemoteChangeNotification没有被解雇

时间:2019-12-03 13:45:08

标签: core-data cloudkit ios13 nspersistentcloudkitcontainer

我正在尝试在使用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被解雇。为了确保我的实现没有错误,我只是在Apple提供的示例代码的@objc func storeRemoteChange(_ notification: Notification)中设置了断点,但是仍然看不到任何通知被触发并且没有断点被激活。

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

5 个答案:

答案 0 :(得分:8)

我的猜测是,您正在观察容器而不是商店协调员,请添加观察者,如下所示:

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

注意最后一个参数container.persistentStoreCoordinator

还有一个警告,该通知出现在所有不同的线程上,因此请谨慎使用并发。只需在方法中放置5秒钟的睡眠,您就会在应用程序启动时看到3个不同的线程调用它。这可能是为什么在示例中有一个historyQueue为1的maxOperationCount处理它的原因。

有些通知在NSPersistentHistoryTokenKey中有userInfo,不确定为什么。

答案 1 :(得分:2)

我能够在项目中的两台设备之间通过iCloud可靠地响应核心数据更改。但是我到达了需要访问变更历史的地步。苹果公司很好地描述了在Consuming Relevant Store Changes

中进行设置的步骤

我一直很高兴地将相关代码复制并粘贴到我的应用程序中。但是NSPersistentStoreRemoteChange通知没有通过。与喜剧一样,时间就是一切。根据{{​​3}}我

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

我正在配置loadPersistentStores(completionHandler :)内部的persistentStoreStoreDescriptions(内部)。因此,痛苦的显而易见的方法是在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")
}

答案 2 :(得分:1)

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

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

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

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

答案 3 :(得分:0)

我不知道这是否是错误。只需下载并运行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 :(得分:0)

SwiftUI

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

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