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 ?? ""
}
}
}
}
}
The accepted answer has a lot of code smell that I feel the need to clarify it before this pollution spreads to SwiftUI development.
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.
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".
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).
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
}
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With