SwiftUI and CoreData: The MVVM Way

When I first started using SwiftUI for some small projects, I immediately appreciated its ease of use and expressiveness. However, it was not all rosy.

As soon as I began to work on some bigger apps — especially the ones that required Core Data — it became clear to me that I would have to give a different structure to the project itself. Otherwise, I would soon get entangled in a very serious maintainability and testing problem.

The Bad, Easy Way

By going through Apple’s documentation and many other answers on Stack Overflow, what you will find is a lot of people placing the FetchRequest property wrapper variable inside a View struct that needs to fetch some data from the Core Data model.

This might be tempting if you’re just starting out, but as you dig a little further into the logic involved with the fetched entities, you will soon realise that not only is it very hard — if not impossible — to test those functions/variables, but you are also no longer following the MVVM pattern. At that point, the View struct knows a lot about the entities and the functions/properties that you defined inside them. Let me show you an example that is easily replicable with other entities:

struct SimpleList: View {
    @FetchRequest(
        entity: Course.entity(),
        sortDescriptors: []
    ) var courses: FetchedResults<Course>

    var overallGpa: Double {
        let numCourses = courses.count
        let gradeSum = courses.map { $0.score }.reduce(0, +)
        return gradeSum / numCourses
    }

    var body: some View {
        ...
    }
}

In this case, I’m fetching all my Course entities that are stored in Core Data and introducing some logic that I will show later in my view. How are we going to test that computed variable? You might make an ad hoc function that you can test later, but that is not a clean way to do it. Plus, you don’t want data logic in any view.

A Clean Alternative

I tend to have a single ViewModel per View so that I can test each component individually and use just what I need in each one of them instead of having a big giant ViewModel to pass around in views.

With SwiftUI 2.0, you will find out that the initial project passes a managedObjectContext as an environmentObject property. We do not want that. It is not clean to take that context every time in each View that needs to fetch data from the database, and I don’t want my views to know about that either.

What I came up with is a more MVVM way of doing this that involves our easy-to-use publishers and subscribers.

Basically, you are going to create a singleton instance that watches over a single entity in the database (in this case, the Course entity) and sends all the courses to each subscriber that subscribes to it. This way, you can easily instantiate multiple ViewModels that subscribe to the storage publisher and operate some logic on those entities in a more dedicated and testable space. Let’s see an example of what CourseStorage would look like:

class CourseStorage: NSObject, ObservableObject {
    var courses = CurrentValueSubject<[Course], Never>([])
    private let courseFetchController: NSFetchedResultsController<Course>

    private override init() {
        courseFetchController = NSFetchedResultsController(
            fetchRequest: Course.Request.all.rawValue,
                managedObjectContext: PersistenceController.shared.container.viewContext,
                sectionNameKeyPath: nil, cacheName: nil
        )

        super.init()

        courseFetchController.delegate = self

        do {
            try courseFetchController.performFetch()
            courses.value = courseFetchController.fetchedObjects ?? []
        } catch {
            NSLog("Error: could not fetch objects")
        }
    }

    func add() {
        ...
    }

    func update() {
        ...
    }

    func delete(id: UUID) {
        ...
    }
}

extension CourseStorage: NSFetchedResultsControllerDelegate {
    public func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
        guard let courses = controller.fetchedObjects as? [Course] else { return }
        logger.log("Context has changed, reloading courses")
        self.courses.value = courses
    }
}

As you can see in the code, I am creating a singleton ObservableObject that exposes a course publisher. It is going to emit a new value when the managedObjectContext entities change. You might have also noticed that I placed the PersistenceController in the class itself. This way, we can forget about passing it around to views, as I’ve explained before. In this class, I am also handling everything that concerns the Core Data operations, such as saving, adding, and deleting entities from the store. As such, I am separating database logic from the app’s business logic.

Here is an example of how the ViewModel can be structured with these changes:

class CourseViewViewModel: ObservableObject {
    @Published var activeCourses: [Course] = []
    @Published var overallGpa: Double = []
    @Published var courses: [Course] = [] {
        willSet {
            activeCourses = newValue.filter { $0.mark == 0 }
            let scoreSum = newValue.map { $0.score }.filter { $0 != 0 }
            let passedCourses = newValue.filter { $0.mark != 0 }
            overallGpa = scoreSum / passedCourses
        }
    }

    private var cancellable: AnyCancellable?

    init(coursePublisher: AnyPublisher<[Course], Never> = CourseStorage.shared.courses.eraseToAnyPublisher()) {
        cancellable = coursePublisher.sink { [unowned self] courses in
            self.courses = courses
        }
    }
}

The ViewModel now encapsulates all the data and logic that it is supposed to handle. The code is pretty much self-explanatory: The CourseViewModel subscribes to the CourseStorage publisher and receives up-to-date course values from it.

You might be wondering why I am initialising the ViewModel with an AnyPublisher value. Remember the testing advantage? It is precisely for this situation. If I now want to test CourseViewModel and its logic, I can just create a sample test array that contains a bunch of courses, pass that as an array publisher to the ViewModel, and make all the asserts necessary for the test.

class TestVM: XCTest {
    func simpleTest() {
        let courses = [[Course(), Course(), ...]].publisher
        let vm = CourseViewModel(coursePublisher: courses)
        ...
    }
}

Final Result

Now, I am going to show you how simple and clean the View struct looks with these changes:

struct SimpleList: View {
    @StateObject private var viewModel = CourseViewModel()

    var body: some View {
        ...
    }
}

No more managedObjectContext passed around views. There is logic separation. It is MVVM-compliant, easily testable, and clean.

Conclusion

I have to say that I am pretty satisfied with the results. My components are now all structured this way and I have not found a single issue with the implementation. This is why I encourage you to try this out so that you can organise big projects (and smaller ones) in a well-structured, maintainable, and more elegant way.

Enjoyed the read?

If you enjoy my writing, please check out my Patreon patreon.com/mattrighetti and become my supporter. Sharing the article is also greatly appreciated.