SwiftUI and MVVM - passing data from a child View's ViewModel to a parent View's ViewModel

Viewed 680

I am wondering how to pass data from one ViewModel to another in a SwiftUI app when one View is a parent of another, but the ViewModels do not have any connection.

CarShowView (uses CarShowViewModel) Presents a sheet of CarSearchView (uses CarSearchViewModel) when a user clicks a button.

The CarSearchViewModel has an array of allCars, from which a user can select a single Car. This Car should then get added to a newCars array in the CarShowViewModel.

If I only needed to grab a single car. But a user could potentially open the SearchView, pick a car to add it to newCars, then reopen the search view and pick another car to add to newCars.

Do I need to have this logic somehow in the View? Or is there a way I can use these ViewModels to talk to each other? There are a lot of questions related to this, but I haven't found one that specifically solves this case.

class CarShowViewModel: ObservableObject {
     @Published var newCars: [Car]
}
class CarShowView: View {
     @ObservedObject var carShowVM = CarShowViewModel()



     var body: some View {
          // body
     }.sheet(isPresented: $showAllCars) {
          CarSearchView(carSearchVM: CarSearchViewModel())
     }
}
class CarSearchViewModel: ObservableObject {
     @Published var chosenCar: Car? = nil
     @Published var allCars: [Car]

     // repository to grab all cars
     var carRepository: CarRepository

     func save(car: Car) {
          // should save the car I select as chosenCar and close the CarSearch. Somehow I need this to pass the car chosen to CarShowViewModel.
     }
}
class CarSearchView {
     @ObservedObject var carSearchVM: CarSearchViewModel

}
1 Answers

One way to accomplish this is by using a callback function that's passed to the second view model that can execute some code in the parent view. Something like:

class CarSearchViewModel: ObservableObject {
    @Published var chosenCar: Car? = nil
    @Published var allCars: [Car]
    
    var saveCallback: (Car) -> Void
    var carRepository: CarRepository
    
    init(carRepository: CarRepository, saveCallback: @escaping (Car) -> Void) {
        self.carRepository = carRepository
        self.saveCallback = saveCallback
    }
    
    func save(car: Car) {
        self.saveCallback(car)
    }
}

class CarShowViewModel: ObservableObject {
     @Published var newCars: [Car] = []
}

struct CarShowView: View {
    @ObservedObject var carShowVM = CarShowViewModel()
    @State private var showAllCars = false
    
     var body: some View {
          Text("Hello, world!")
            .sheet(isPresented: $showAllCars) {
                CarSearchView(carSearchVM:
                                CarSearchViewModel(carRepository: CarRepository(), //not sure where this is coming from
                                                   saveCallback: { car in
                                                    carShowVM.newCars = [car] //or whatever should be here
                                                   }))
            }
     }
}

Note that I had to guess a little bit about what's going on here, but the concept should be sound. Also probably important to note that SwiftUI Views should be structs, and not classes -- you have them marked incorrectly in your code.


Another method to accomplish this would be to have one source of truth for both the parent and child view -- basically, one view model for both. This may break a strict MVVM pattern, of course.

There are also ways you could do this with Combine and Publishers, but the callback method I mentioned seems the simplest.

Related