Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

SwiftUI get values of another ViewModel in a ViewModel

I have these filters in my view and all of them update the FilterViewModel which then takes care of filtering the data. One of these views, SearchAddressView expects a PlacemarkViewModel and not a FilterViewModel because it provides a dropdown list of addresses when user starts typing. There is a lot of code there so I do not want to duplicate this code into my FilterViewModel

However, I need to read @Published var placemark: Placemark from PlacemarkViewModel to FilterViewModel. I'm trying to import PlacemarkViewModel into FilterViewModel and then to use didSet { } to read it's values but its not working.

So the idea is .. while the users searches for an address this updates the PlacemarkViewModel but FilterViewModel needs to get this value also. Any idea on how to achieve this?

struct FiltersView: View {
    @ObservedObject var filterViewModel: FilterViewModel

    var body: some View {
        ScrollView(.horizontal, showsIndicators: false) {
            HStack {
                FilterButtonView(title: LocalizedStringKey(stringLiteral: "category"), systemName: "square.grid.2x2.fill") {
                    CategoryFilterView(filterViewModel: self.filterViewModel)
                }

                FilterButtonView(title: LocalizedStringKey(stringLiteral: "location"), systemName: "location.fill") {
                    SearchAddressView(placemarkViewModel: self.filterViewModel.placemarkViewModel)
                }

                FilterButtonView(title: LocalizedStringKey(stringLiteral: "sort"), systemName: "arrow.up.arrow.down") {
                    SortFilterView(filterViewModel: self.filterViewModel)
                }
            }
        }
    }
}

FilterViewModel

class FilterViewModel: ObservableObject, LoadProtocol {
    @Published var placemarkViewModel: PlacemarkViewModel() {
           didSet {
            print("ok") // nothing
           }
       }
}

PlacemarkViewModel

class PlacemarkViewModel: ObservableObject {
    let localSearchCompleterService = LocalSearchCompleterService()
    let locationManagerService = LocationManagerService()
    @Published var addresses: [String] = []

    // I need this value in my FilterViewMode;
    @Published var placemark: Placemark? = nil
    @Published var query = "" {
        didSet {
            localSearchCompleterService.autocomplete(queryFragment: query) { (addresses: [String]) in
                self.addresses = addresses
            }
        }
    }

    init(placemark: Placemark? = nil) {
        self.placemark = placemark
    }

    var address: String {
        if let placemark = placemark {
            return "\(placemark.postalCode) \(placemark.locality), \(placemark.country)"
        }

        return ""
    }

    func setPlacemark(address: String) {
        locationManagerService.getLocationFromAddress(addressString: address) { (coordinate, error) in
            let location: CLLocation = CLLocation(latitude: coordinate.latitude, longitude: coordinate.longitude)
            self.locationManagerService.getAddressFromLocation(location: location) { (placemark: CLPlacemark?) in
                if let placemark = placemark {
                    self.placemark = Placemark(placemark: placemark)
                    self.query = placemark.name ?? ""
                }
            }
        }
    }

    func getAddressFromLocation() {
        locationManagerService.getLocation { (location: CLLocation) in
            self.locationManagerService.getAddressFromLocation(location: location) { (placemark: CLPlacemark?) in
                if let placemark = placemark {
                    self.placemark = Placemark(placemark: placemark)
                    self.query = placemark.name ?? ""
                }
            }
        }
    }
}
like image 709
M1X Avatar asked Oct 30 '25 15:10

M1X


2 Answers

The accepted answer has a lot of code smell that I feel the need to clarify it before this pollution spreads to SwiftUI development.

  • Nested view model should not be encouraged in any way.

View model itself is ambiguous (why is control logic / side effects allowed in it?)

Nested view model? What does that even mean? And again there's nothing preventing you from stashing side effects in it, which is harder to trace and debug.

You also need to consider the costs to maintain object life-cycle (init, pass-around nested, retain-cycle, release).

e.g; init(placemarkViewModel: PlacemarkViewModel) { self.placemarkViewModel = placemarkViewModel }

The argument I've seen regarding nested view model is that "it's a common practice".

No, it's a common mistake. How do you feel when someone writes this?

`vm1.vm2.vm3.modelY.property1.vm.p2`

Because that's exactly what would happen when you encourage this.

  • network call with side effect in init()

MVVM often treats ViewModel like a harmless value type, when it is in fact a reference type filled with Control / business logic / side effects.

This is one such example. When you create a "model", you initiate a network request with side effect. This would hurt unaware developer that uses your "model".

  • Decouple networking and use value type

Networking should not be the only reason you make a reference type model. You can have dedicated networking service object and value type model.

If you strip all networking from "ViewModel", and you find the remaining "ViewModel" to be trivial or silly, then you are on the right track.

Instead of having two view models and have implicit dependencies, you should use @EnvironmentObject.

e.g.;

final class SharedState: ObservableObject {
    @Published var placemark: Placemark?
    // other stuff you want to publish

    func updatePlacemark() {
        // network call to update placemark and trigger view update
    }

}
let state = SharedState()
state.updatePlacemark() // explicit call for networking with side effects
// set as environmentObject, e.g.; ContentView().environmentObject(state)

Your SearchAddressView can remove outside parameter and access environmentObject directly.

So you can remove all the view models passing around:

FilterButtonView(title: LocalizedStringKey(stringLiteral: "location"), systemName: "location.fill") {
                SearchAddressView()
}

Wait, but that makes my fancy view models useless?

This may be the greatest take-away in all this. You don't need them. They introduce an extra layer of complexity that do more harm than good (you being stuck for example).

like image 168
Jim lai Avatar answered Nov 01 '25 07:11

Jim lai


Here is possible approach

class FilterViewModel: ObservableObject, LoadProtocol {
    @Published var placemarkViewModel = PlacemarkViewModel()

    private var cancellable: AnyCancellable? = nil
    init() {
        cancellable = placemarkViewModel.$placemark
            .sink { placemark in
                // handle updated placemark here
            }
    }
}
like image 33
Asperi Avatar answered Nov 01 '25 06:11

Asperi



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!