Как: использовать Combine для реагирования на изменения CoreData в фоновом режиме - PullRequest
2 голосов
/ 02 марта 2020

Я хочу добиться следующего: всякий раз, когда кто-то запускает сохранение CoreData (ie. 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()

Я также подписался на уведомление через NotificationCenter.default.publisher(for: .NSManagedObjectContextDidSave) и получаю уведомление о сохранении дважды после запуска только one managedObjectContext.save(). Однако оба уведомления отправляются из одного потока (который является UIThread), и все NSManagedObjects в пользовательском словаре имеют .managedObjectContext, то есть viewContext, а не backgroundContext.

My Идея состояла в том, чтобы отфильтровать уведомления, основываясь на том, был ли связанный NSManagedObjectContext фоновым, поскольку я предполагал, что уведомление также отправляется в (частном) DispatchQueue, но кажется, что все уведомления отправляются в UIThread, и фоновый контекст никогда не использовал.

Есть идеи, как решить эту проблему? Это ошибка? Как я могу получить уведомления на основе backgroundContext с последующими задачами, выполняемыми в соответствующем DispatchQueue?

Ответы [ 2 ]

2 голосов
/ 05 марта 2020

Вы можете создать издателя, который будет информировать вас, когда что-то релевантное для вас в Базовых данных изменилось.

Я написал статью об этом. Объединение, издатели и основные данные.

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

}
1 голос
/ 04 марта 2020

Вы можете передать объект, который хотите наблюдать, на publisher(for:):

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

. Он будет прослушивать только уведомления, связанные с фоновым контекстом управляемого объекта, что означает, что вы можете безопасно выполнять обработку в очереди этого контекста. .

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...