【问题标题】:How to: Using Combine to react to CoreData changes in the background如何:使用 Combine 在后台对 CoreData 更改做出反应
【发布时间】:2020-06-15 03:23:47
【问题描述】:

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

目前的问题在于 NSManagedObject 上下文明确绑定到线程,不鼓励您在该线程之外使用NSManagedObjects。

我在SceneDelegate 中设置了两个NSManagedObjectContexts:

let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
let backgroundContext = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.newBackgroundContext()

我还通过NotificationCenter.default.publisher(for: .NSManagedObjectContextDidSave) 订阅了通知,并且在我仅触发一个 managedObjectContext.save() 后收到了保存通知两次。但是,两个通知都是从同一个线程(即 UIThread)发送的,并且用户字典中的所有NSManagedObjects 都有一个.managedObjectContext,即viewContext,而不是backgroundContext

我的想法是根据关联的NSManagedObjectContext 是否为背景来过滤通知,因为我假设通知也在(私有)DispatchQueue 上发送,但似乎所有通知都在 UIThread 上发送并且从不使用背景上下文。

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

【问题讨论】:

  • publisher(for:) 有第二个参数 object,默认为 nil。尝试将 object 参数设置为您要观察的上下文。 Docs
  • 保留背景上下文不是一个好主意,否则会充满不必要的对象

标签: ios swift core-data swift5 combine


【解决方案1】:

如果您没有两次保存上下文,那么您必须添加两次观察者。

【讨论】:

    【解决方案2】:

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

    我为此写了一篇文章。 Combine, Publishers and 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
            }
        }
    
    }
    

    【讨论】:

    • 真的很喜欢这段代码,如何编写自定义发布者的好例子。我刚刚注意到过时的synchronized()objc_sync_enter()/..exit() 可以替换为串行队列或类似的东西:)
    【解决方案3】:

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

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

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

    【讨论】:

    • 我将接受这个答案,因为它确实过滤了正确的上下文,并且后续操作在正确的线程上执行。但我想在这里留下信息,在我的场景中,你没有收到后台线程的NSManagedObjectContextDidSave 通知(可能因为它没有保存,它只是得到了更新)。所以你必须改用NSManagedObjectContextObjectsDidChange
    猜你喜欢
    • 2015-05-09
    • 1970-01-01
    • 2015-12-30
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-08-09
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多