13

我正在尝试在我的 CoreData+CloudKit 项目中执行历史跟踪,该项目使用NSPersistentCloudKitContainer. 我一直在关注 Apple 的示例项目

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

我已经为我的项目启用了历史跟踪,如 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 实施中的错误还是我缺少任何所需的设置?

4

5 回答 5

17

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

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

注意最后一个参数container.persistentStoreCoordinator

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

有些通知有NSPersistentHistoryTokenKeyuserInfo不确定为什么。

于 2020-02-09T23:30:54.580 回答
4

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

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

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

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

于 2020-01-03T06:28:33.773 回答
3

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

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

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

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")
}
于 2020-08-29T20:49:03.260 回答
2

不知道是不是bug。只需下载并运行 Apple 的示例项目,但从NSPersistentStoreRemoteChangeNotification未被解雇。

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

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

这是我在 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)
}
于 2020-02-12T04:27:31.537 回答
2

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
    }
}
于 2021-08-03T18:05:50.893 回答