What's the recommended code structure to manage apps with MVVM+SwiftUI+CoreData

I have a SwiftUI app with 3 Core Data entities, Car, CarService and ServiceRecord where Car has many carServices and each CarService has many serviceRecords. Everything is working fine but I'm not sure what's the most common MVVM practice.

As you can see in the following example I'm using CoreDataViewModel to fetch data from Core Data and then I pass it around all SwiftUI views, at the moment I don't have a ViewModel for the view's logic (the logic is currently inside of each view) but that's something I would like to incorporate, but I'm not sure what would be the best way to do that. I'm thinking about the two following option...

  1. Create a view model for each view (CarViewModel, ServicesViewModel and RecordsViewModel) to handle ONLY the view's logic and leave the existing CoreDataViewModel as is.

  2. Create a view model for each view, CarViewModel, ServicesViewModel and RecordsViewModel to handle the logic for each view and move the CoreData quests to each of the view models respectably and basically delete CoreDataViewModel altogether since now the core data related will live inside each view model.

Which of the two options above makes more sense for an MVVM app?

In general, can someone please share how you usually structure your code when using MVVM + CoreData + SwiftUI?

CoreDataManager

class CoreDataManager{

    static let instance = CoreDataManager()

    lazy var context: NSManagedObjectContext = {
        return container.viewContext
    }()
    
    lazy var container: NSPersistentContainer = {
        return setupContainer()
    }()
    
    func setupContainer()->NSPersistentContainer{
        // code to setup container...
        return container
    }
    
    func save(){
        do{
            try context.save()
        }catch let error{
            print("Error saving Core Data. \(error.localizedDescription)")
        }
    }
}

CoreDataViewModel

class CoreDataViewModel: ObservableObject{

    let manager: CoreDataManager
    
    @Published var cars: [Car] = []
    @Published var carServices: [CarService] = []
    @Published var serviceRecords: [ServiceRecord] = []
    
    init(coreDataManager: CoreDataManager = .instance){
        self.manager = coreDataManager
        // getCars() etc.
    }

    // CREATIONS
    func addCar(name:String){}
    func addService(name:String, cost: Double){}
    func createRecord(name:String, cost: Double){}
    
    // DELETES
    func deleteCar(){}
    func deleteCarService(){}
    func deleteServiceRecord(){}
    
    // UPDATES
    func updateCar(){}
    func updateService(){}

    // GETS
    func getCars(){}
    func getServices(){}
    func getRecords(){}
    
    func save(){
        self.manager.save()
    }
}

SwiftUI Views

CarsView

struct CarsView: View {
    @StateObject var coreDataViewModel = CoreDataViewModel()
    var body: some View {
        NavigationView{
            VStack{
                List {
                    ForEach(coreDataViewModel.cars) { car in
                    }
                }
            }  
        }
    }
}

ServicesView

struct ServicesView: View {
    @ObservedObject var coreDataViewModel:CoreDataViewModel
    var body: some View {
        NavigationView{
            VStack{
                List {
                    ForEach(coreDataViewModel.carServices) { service in
                    }
                }
            }  
        }
    }
}

RecordsView

struct RecordsView: View {
    @ObservedObject var coreDataViewModel: CoreDataViewModel
    var body: some View {
        NavigationView{
            VStack{
                List {
                    ForEach(coreDataViewModel.serviceRecords) { record in
                    }
                }
            }  
        }
    }
}

Thanks!

hi,

i like your question:

Which of the two options above makes more sense for an MVVM app?

i have used one, both, sometimes neither, and sometimes variations, and i have not really liked any enough to say that "that's what i'd do" the next time i work on an App.

some thoughts:

(1) my bottom-line goal is to be sure that no ShiftUI View should be in the business of really knowing that the (class) objects it works with are Core Data objects. for example: i don't ever want a View executing code that uses the managed object context to create a new object or delete an existing object. so some form of VM is needed, whether it be a global singleton or a locally-created @StateObject on a per-view basis.

(2) yet a second goal is to avoid building a MVM = "Massive View Model." because you are using Core Data objects, i think your first option might be better, because you have relationships and fetch requests to manage. i think those are better done and centralized by some object that has a global view of the database.

(3) remember that with Core Data relationships in play, there are some subtleties that should be handled in a VM. for example: when updating an existing CarService, perhaps in a detail view, the VM will know that it must manually execute carService.car?.objectWillChange.send() so that the associate Car record should be treated as having changed (changing a field in CarService does not do this for you), because you probably have some Views in your app that hold an @ObservedObject Car object reference.

(4) you have the decision as to whether you want to use @FetchRequests directly to deliver an array of objects to a view, or have a VM use its own NSFetchedResultsController objects to deliver that array.

other than for simple views, such as a List, i have usually chosen the latter. however, even then, i've started to dislike having to begin every SwiftUI view file with the usual litany of property wrappers, one of which is a @FetchRequest that's repeated verbatim in multiple views throughout the app.

(5) i usually add a few convenience extensions to the Core Data classes to do simple things, such as nil-coalesce optional fields of the Core Data record, convert between Int and Int32/Int16, or provide a computed variable (one that's especially useful is to return a properly typed Set or Array of CarService objects for a given Car).

but i have found it useful to also include creation and deletion functions in that extension as class functions on the Core Data class itself.

so instead of writing a "let newService = CarService(context: contact) ..." sequence that defaults and hooks up the new service with an existing Car, i will write a single statement "let newService = CarService.add(to: car, ...)" and leave the details to the CarService class to do the creation, default all the fields, and hook the service to the Car. this simplifies the call site, and is especially useful if there are several points in your app where you add services to a car.

hope some of this helps,

DMG

forgot to mention: i have posted some code where you can see my evolution in thinking on these ideas.

What's the recommended code structure to manage apps with MVVM+SwiftUI+CoreData
 
 
Q