私は次のことを実現したいと思います。誰かがCoreData保存をトリガーするたびに(つまり、NSManagedObjectContextDidSave
通知が送信される)、変更されたNSManagedObjectに基づいてbackground計算を実行したいと思います。具体例:ノートアプリで、すべてのノートの単語の総数を非同期で計算したいとします。
現在の問題は、NSManagedObjectコンテキストが明示的にスレッドにバインドされており、このスレッドの外でNSManagedObject
sを使用しないことが推奨されているという事実にあります。
NSManagedObjectContext
に2つのSceneDelegate
sを設定しました:
let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext let backgroundContext = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.newBackgroundContext()
また、NotificationCenter.default.publisher(for: .NSManagedObjectContextDidSave)
を介して通知をサブスクライブし、トリガーした後twiceのみ保存通知を受信していますonemanagedObjectContext.save()
。ただし、両方の通知は同じスレッド(UIThread)から送信され、ユーザー辞書のすべてのNSManagedObjects
には.managedObjectContext
はviewContext
ではなくbackgroundContext
です。
私の考えは、関連付けられたNSManagedObjectContext
がバックグラウンドの通知であるかどうかに基づいて通知をフィルタリングすることでした。通知は(プライベート)DispatchQueueでも送信されると想定しましたが、すべての通知はUIThreadで送信され、バックグラウンドコンテキストは使用されません。
これを解決する方法について何か考えはありますか?これはバグですか? backgroundContext
に基づいて通知を取得するには、関連するDispatchQueueでダウンストリームタスクを実行しますか?
監視したいオブジェクトをpublisher(for:)
に渡すことができます:
NotificationCenter.default
.publisher(for: .NSManagedObjectContextDidSave, object: backgroundMoc)
.sink(receiveValue: { notification in
// handle changes
})
これは、バックグラウンド管理対象オブジェクトコンテキストに関連する通知のみをリッスンします。つまり、そのコンテキストのキューで安全に処理を実行できます。
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
}
}
}