Observing CoreData fetch request results from view models in SwiftUI

Observing CoreData fetch request results from view models in SwiftUI

CoreData and SwiftData provide property wrappers that allow us to react to data changes (@FetchRequest and @Query respectively) unfortunately, if you want to move away the data handling from the view as it's the case with MVVM, you won't be able to use these property wrappers as they can only be used from Views.

To get around that limitation we can use a NSFetchedResultsController and conform to NSFetchedResultsControllerDelegate to be notified about changes and with Observation we can expose the results to drive UI changes.

@Observable
final class ObservableFetchRequest<M: NSManagedObject>:
    NSObject,
    NSFetchedResultsControllerDelegate
{
    private(set) var fetchedObjects: [M] = []
    private let fetchController: NSFetchedResultsController<M>

    init(
        context: NSManagedObjectContext,
        predicate: NSPredicate? = nil,
        sortDescriptors: [NSSortDescriptor] = []
    ) {
        guard let request = M.fetchRequest() as? NSFetchRequest<M> else {
            fatalError("Failed to create fetch request for \(M.self)")
        }

        if let predicate {
            request.predicate = predicate
        }

        request.sortDescriptors = sortDescriptors

        fetchController = NSFetchedResultsController<M>(
            fetchRequest: request,
            managedObjectContext: context,
            sectionNameKeyPath: nil,
            cacheName: nil
        )

        super.init()

        fetchController.delegate = self
    }

    func fetch() throws {
        try fetchController.performFetch()
    }

    func setPredicate(_ predicate: NSPredicate) throws {
        fetchController.fetchRequest.predicate = predicate
        try fetch()
    }

    func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChangeContentWith snapshot: NSDiffableDataSourceSnapshotReference) {
        fetchedObjects = fetchController.fetchedObjects ?? []
    }
}

Being able to handle fetch requests from the view model means that we can keep all the logic that affects the fetched data in a single place and use unit tests to validate it.