I have a couple of places in my SwiftUI app where I need to fetch a (growing) set of information from CoreData and process it before updating the UI.
I am using the MVVM approach for this and so have a number of NSFetchedResultsController instances looking after fetching and updating results.
In order to stop blocking the main thread as much I tried shifting the NSFetchedResultsController to perform its work on a background thread instead.
This works but only if I disable the -com.apple.CoreData.ConcurrencyDebug 1 argument to ensure I'm not breaching threading rules.
I am already ensuring that all CD access is done on the background thread however it appears that when the SwiftUI view accesses a property from the CD object it is doing so on the main thread and so causing a crash.
For now I can think of a couple of possible solutions:
- Ensure that the data fetched by the
NSFetchedResultsControllercan be fetched/processed in a small amount of time to ensure the UI doesn't hang - Make "DTO" objects so that data fetched is then inserted into another class instance inside the background thread and have the UI use that.
- The issue with this approach is then making edits or reacting to updates on the object become a lot more convoluted as you need to manually keep the CD and the DTO objects in-sync.
EDIT: Added example of ViewModel I'm using
class ContentViewModel: NSObject, ObservableObject, NSFetchedResultsControllerDelegate {
@Published var items: [Item] = []
private let viewContext = PersistenceController.shared.container.viewContext
private let resultsController: NSFetchedResultsController<Item>!
private let backgroundContext: NSManagedObjectContext!
override init() {
let fetchRequest: NSFetchRequest<Item> = Item.fetchRequest()
let sort = NSSortDescriptor(keyPath: \Item.timestamp, ascending: true)
fetchRequest.sortDescriptors = [sort]
fetchRequest.propertiesToFetch = ["timestamp"]
backgroundContext = PersistenceController.shared.container.newBackgroundContext()
backgroundContext.automaticallyMergesChangesFromParent = true
resultsController = NSFetchedResultsController(
fetchRequest: fetchRequest,
managedObjectContext: backgroundContext,
sectionNameKeyPath: nil,
cacheName: nil)
super.init()
resultsController.delegate = self
try? resultsController.performFetch()
DispatchQueue.main.async { [self] in
items = resultsController.fetchedObjects ?? []
}
}
func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
DispatchQueue.main.async { [self] in
withAnimation {
items = (controller.fetchedObjects as? [Item]) ?? []
}
}
}
}
NSFetchedResultsControllerdoes it has to be on the main thread. You haven't posted a Minimal, Reproducible Example (MRE), so it is difficult to comment further.Itemproperties inside the view