I have created a simple Core Data project at Github to demonstrate my problem:
My test app downloads a JSON list of objects, stores it in Core Data and displays in a SwiftUI List via @FetchRequest
.
Because the list of objects has 1000+ elements in my real app, I would like to save the entities into the Core Data on a background thread and not on the main thread.
Preferably I would like to use the same default background thread, which is already used by the URLSession.shared.dataTaskPublisher.
So in the standard Persistence.swift generated by Xcode I have added only 2 lines:
container.viewContext.mergePolicy = NSMergePolicy.mergeByPropertyObjectTrump
container.viewContext.automaticallyMergesChangesFromParent = true
and in my DownloadManager.swift singleton I call:
static let instance = DownloadManager()
var cancellables = Set<AnyCancellable>()
// How to run this line on the background thread of URLSession.shared.dataTaskPublisher?
let backgroundContext = PersistenceController.shared.container.newBackgroundContext()
private init() {
getTops()
}
func getTops() {
guard let url = URL(string: "https://slova.de/ws/top") else { return }
URLSession.shared.dataTaskPublisher(for: url)
.tryMap(handleOutput)
.decode(type: TopResponse.self, decoder: JSONDecoder())
.sink { completion in
print(completion)
} receiveValue: { [weak self] returnedTops in
for top in returnedTops.data {
// the next line fails with EXC_BAD_INSTRUCTION
let topEntity = TopEntity(context: self!.backgroundContext)
topEntity.uid = Int32(top.id)
topEntity.elo = Int32(top.elo)
topEntity.given = top.given
topEntity.avg_score = top.avg_score ?? 0.0
}
self?.save()
}
.store(in: &cancellables)
}
As you can see in the above screenshot, this fails with
Thread 4: EXC_BAD_INSTRUCTION (code=EXC_I386_INVOP, subcode=0x0)
because I have added the following "Arguments Passed on Launch" in Xcode:
-com.apple.CoreData.ConcurrencyDebug 1
Could anyone please advise me, how to call the newBackgroundContext()
on the proper thread?
UPDATE:
I have tried to workaround my problem as in below code, but the error is the same:
URLSession.shared.dataTaskPublisher(for: url)
.tryMap(handleOutput)
.decode(type: TopResponse.self, decoder: JSONDecoder())
.sink { completion in
print(completion)
} receiveValue: { returnedTops in
let backgroundContext = PersistenceController.shared.container.newBackgroundContext()
for top in returnedTops.data {
// the next line fails with EXC_BAD_INSTRUCTION
let topEntity = TopEntity(context: backgroundContext)
topEntity.uid = Int32(top.id)
topEntity.elo = Int32(top.elo)
topEntity.given = top.given
topEntity.avg_score = top.avg_score ?? 0.0
}
do {
try backgroundContext.save()
} catch {
let nsError = error as NSError
fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
}
UPDATE 2:
I was assuming that when newBackgroundContext()
is called, it takes the current thread and then you can use that context from the same thread...
This seems not to be the case and I have to call perform
, performAndWait
or performBackgroundTask
(I have updated my code at Github to do just that).
Still I wonder, if the thread of newBackgroundContext
can be the same as of the URLSession.shared.dataTaskPublisher
...
It seems like you have figured most of this out yourself.
This seems not to be the case and I have to call perform,
performAndWait
orperformBackgroundTask
(I have updated my code at Github to do just that).
What's still not solved is here -
Still I wonder, if the thread of
newBackgroundContext
can be the same as of theURLSession.shared.dataTaskPublisher...
The URLSession
API allows you to provide a custom response queue like following.
URLSession.shared
.dataTaskPublisher(for: URL(string: "https://google.com")!)
.receive(on: DispatchQueue(label: "API.responseQueue", qos: .utility)) // <--- HERE
.sink(receiveCompletion: {_ in }, receiveValue: { (output) in
print(output)
})
OR traditionally like this -
let queue = OperationQueue()
queue.underlyingQueue = DispatchQueue(label: "API.responseQueue", qos: .utility)
let session = URLSession(configuration: .default, delegate: self, delegateQueue: queue)
So ideally, we should pass in the NSManagedObjectContext
's DispatchQueue
as the reponse queue for URLSession
.
The issue is with NSManagedObjectContext
API that -
DispatchQueue
instance from outside.DispatchQueue
instance.We can't access underlying DispatchQueue
for NSManagedObjectContext
instance. The ONLY EXCEPTION to this rule is .viewContext
that uses DispatchQueue.main
. Of course we don't want to handle network response decoding and thousands of records being created/persisted on/from main thread.