0

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 NSFetchedResultsController can 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]) ?? []
            }
        }
    }
}
2
  • 1
    You can process data on a background thread, but if you are updating the UI, like NSFetchedResultsController does it has to be on the main thread. You haven't posted a Minimal, Reproducible Example (MRE), so it is difficult to comment further. Commented Jun 15, 2023 at 16:35
  • @Yrb thanks, I've edited with an example of the ViewModel basically adapted from the Core Data Xcode template. The crash only happens when I have the concurrency debug flag enabled but it whenever I refer to any of the Item properties inside the view Commented Jun 15, 2023 at 20:07

1 Answer 1

1

The core problem is that you're fetching items on a background queue and then using them on the main queue. That's not allowed with Core Data. It might not crash immediately without the concurrency debug flag, but it's setting up a crash that's likely to happen at some point-- which is why concurrency debugging complains.

To fetch on one queue but use the results on the other, you can use the object IDs. They're thread safe. You'd use something like

@Published var itemIDs: [NSManagedObjectID] = []

And follow that with something like

itemIDs = resultsController.fetchedObjects.map { $0.objectID } ?? []

Then over in the main queue, look up objects by their IDs. You can do this for an array of objects by doing a fetch where the predicate is something like NSPredicate(format: "self in %@", itemIDs). It should be fast because there's no need do any filtering at that point and because of internal Core Data caching.

Sign up to request clarification or add additional context in comments.

1 Comment

I think instead of performing a fetch request, you could also iteratively fetch the items using object(with:). Might be a bit cleaner, but I have to check whether there are any performance differences between the two approaches.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.