如何:使用 Combine 在后台对 CoreData 更改做出反应

Nic*_*las 9 core-data ios swift swift5 combine

我想实现以下目标:每当有人触发 CoreData 保存(即NSManagedObjectContextDidSave发送通知)时,我想根据更改的 NSManagedObject执行一些后台计算。具体示例:假设在一个笔记应用程序中,我想异步计算所有笔记中的总字数。

当前的问题在于 NSManagedObject 上下文显式绑定到线程,并且不鼓励您NSManagedObject在该线程之外使用s。

NSManagedObjectContext在我的 s 中设置了两个SceneDelegate

let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
let backgroundContext = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.newBackgroundContext()
Run Code Online (Sandbox Code Playgroud)

我也都签署通过的通知NotificationCenter.default.publisher(for: .NSManagedObjectContextDidSave),并正在接收保存的通知两次我只触发后一个 managedObjectContext.save()。但是,这两个通知都是从同一个线程(即 UIThread)发送的,并且NSManagedObjects用户字典中的所有通知都有一个.managedObjectContext是 theviewContext而不是backgroundContext

我的想法是根据关联NSManagedObjectContext是否为后台来过滤通知,因为我认为通知也是在(私有)DispatchQueue 上发送的,但似乎所有通知都是在 UIThread 上发送的,并且从未使用过后台上下文.

关于如何解决这个问题的任何想法?这是一个错误吗?如何根据backgroundContext正在关联的 DispatchQueue 上运行的下游任务检索通知?

tal*_*lis 14

您可以创建一个发布者,当 Core Data 中与您相关的内容发生变化时,它会通知您。

我写了一篇关于这个的文章。合并、发布者和核心数据。

import Combine
import CoreData
import Foundation

class CDPublisher<Entity>: NSObject, NSFetchedResultsControllerDelegate, Publisher where Entity: NSManagedObject {
    typealias Output = [Entity]
    typealias Failure = Error

    private let request: NSFetchRequest<Entity>
    private let context: NSManagedObjectContext
    private let subject: CurrentValueSubject<[Entity], Failure>
    private var resultController: NSFetchedResultsController<NSManagedObject>?
    private var subscriptions = 0

      init(request: NSFetchRequest<Entity>, context: NSManagedObjectContext) {
        if request.sortDescriptors == nil { request.sortDescriptors = [] }
        self.request = request
        self.context = context
        subject = CurrentValueSubject([])
        super.init()
    }

      func receive<S>(subscriber: S)
        where S: Subscriber, CDPublisher.Failure == S.Failure, CDPublisher.Output == S.Input {
        var start = false

        synchronized(self) {
            subscriptions += 1
            start = subscriptions == 1
        }

        if start {
            let controller = NSFetchedResultsController(fetchRequest: request, managedObjectContext: context, 
                                                        sectionNameKeyPath: nil, cacheName: nil)
            controller.delegate = self

            do {
                try controller.performFetch()
                let result = controller.fetchedObjects ?? []
                subject.send(result)
            } catch {
                subject.send(completion: .failure(error))
            }
            resultController = controller as? NSFetchedResultsController<NSManagedObject>
        }
        CDSubscription(fetchPublisher: self, subscriber: AnySubscriber(subscriber))
    }

      func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
        let result = controller.fetchedObjects as? [Entity] ?? []
        subject.send(result)
    }

      private func dropSubscription() {
        objc_sync_enter(self)
        subscriptions -= 1
        let stop = subscriptions == 0
        objc_sync_exit(self)

        if stop {
            resultController?.delegate = nil
            resultController = nil
        }
    }

    private class CDSubscription: Subscription {
        private var fetchPublisher: CDPublisher?
        private var cancellable: AnyCancellable?

        @discardableResult
        init(fetchPublisher: CDPublisher, subscriber: AnySubscriber<Output, Failure>) {
            self.fetchPublisher = fetchPublisher

            subscriber.receive(subscription: self)

            cancellable = fetchPublisher.subject.sink(receiveCompletion: { completion in
                subscriber.receive(completion: completion)
            }, receiveValue: { value in
                _ = subscriber.receive(value)
            })
        }

        func request(_ demand: Subscribers.Demand) {}

        func cancel() {
            cancellable?.cancel()
            cancellable = nil
            fetchPublisher?.dropSubscription()
            fetchPublisher = nil
        }
    }

}
Run Code Online (Sandbox Code Playgroud)

  • 真的很喜欢这段代码,这是关于如何编写自定义发布者的好例子。我刚刚注意到过时的 `synchronized()` 和 `objc_sync_enter()`/`..exit()` 可以用串行队列或类似的东西替换:) (2认同)

don*_*als 7

您可以将要观察的对象传递给publisher(for:)

NotificationCenter.default
  .publisher(for: .NSManagedObjectContextDidSave, object: backgroundMoc)
  .sink(receiveValue: { notification in
    // handle changes
  })
Run Code Online (Sandbox Code Playgroud)

这只会侦听与后台托管对象上下文相关的通知,这意味着您可以安全地对该上下文的队列进行处理。

  • 我将接受这个答案,因为它确实过滤了正确的上下文,并且后续操作在正确的线程上执行。但我想在这里留下信息,在我的场景中,您不会收到后台线程的“NSManagedObjectContextDidSave”通知(可能是因为它没有保存,它只是得到了更新)。所以你必须听“NSManagedObjectContextObjectsDidChange”。 (2认同)