I have a view model that is parent to other children view models. That is:
public class ViewModel: ObservableObject {
@Published var nav = NavigationViewModel()
@Published var screen = ScreenViewModel()
The other children view model, such as nav and screen, all serve a specific purpose. For example, nav’s responsibility is to keep track of the current screen:
class NavigationViewModel: ObservableObject {
// MARK: Publishers
@Published var currentScreen: Screen = .Timeline
}
The ViewModel is instantiated in the App struct:
@main
struct Appy_WeatherApp: App {
// MARK: Global
var viewModel = ViewModel()
// MARK: -
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(viewModel)
}
}
}
And I declare an @EnvironmentObject for it on any view that needs access to it:
@EnvironmentObject var viewModel: ViewModel
Any view referencing a non-object property of ViewModel that is being @Published
whose value changes will result in the view to be re-rendered as expected. However, if the currentScreen @Published
property of the NavigationViewModel
changes, for example, then the view is not being re-rendered.
I know I can make it work if I separate NavigationViewModel
from ViewModel, instantiate it at the app level and use it as its own environment object in any views that access any of its published properties.
My question is whether the above workaround is actually the correct way to handle this, and/or is there any way for views to be subscribed to value changes of properties inside child objects of environment objects? Or is there another way that I’ve not considered that’s the recommended approach for what I’m trying to achieve through fragmentation of view model responsibilities?
There are several ways to achieve this.
Using Combine
.
import Combine
public class ViewModel: ObservableObject {
@Published var nav = NavigationViewModel()
var anyCancellable: AnyCancellable?
init() {
anyCancellable = nav.objectWillChange.sink { _ in
self.objectWillChange.send()
}
}
}
You basically just listen to whenever your navigationViewModel
publishes changes. If so, you tell your views that your ViewModel
has changes aswell.
I suppose due to the name NavigationViewModel
, that you would use it quite often inside other view models?
If that's the case, I would go for a singleton pattern, like so:
class NavigationViewModel: ObservableObject {
static let shared = NavigationViewModel()
private init() {}
@Published var currentScreen: Screen = .Timeline
}
Inside your ViewModel
:
public class ViewModel: ObservableObject {
var nav: NavigationViewModel { NavigationViewModel.shared }
}
You can of course also call it inside any View
:
struct ContentView: View {
@StateObject var navigationModel = NavigationModel.shared
}
You might have to call objectWillChange.send()
after changing publishers.
@Published var currentScreen: Screen = .Timeline {
didSet {
objectWillChange.send()
}
}