My goal is to create Feature Oriented App. i.e. Feature1, Feature2.
My expected result:
ContentView
navigate toFeature1View
:- initialize
Feature1ViewModel
once - navigate to
Feature1_1View
:- initialize
Feature1_1ViewModel
once.
- initialize
- initialize
The problem:
ContentView
navigate toFeature1View
:- initialize
Feature1ViewModel
twice and deinit once. - navigate to
Feature1_1View
:- initialize
Feature1_1ViewModel
once.
- initialize
- initialize
What I’ve tried:
- use
static var shared
. The problem: How to detect the app have exit Feature1 (.onDisappear()
is triggered when navigating from Feature1View to Feature1_1View)
class Feature1ViewModelManager {
static var shared: Feature1ViewModel! {
get {
if sharedClosure == nil {
sharedClosure = Feature1ViewModel()
}
return sharedClosure
}
set {
sharedClosure = newValue
}
}
private static var sharedClosure: Feature1ViewModel!
}
- use the
if else statement
instead of usingNavigationStack
inContentView
. The problem is that I can’t navigate back fromFeature1View
toContentView
Feature1View.swift
enum Feature1ViewRoute {
case Feature1_1
}
class Feature1ViewModel: ObservableObject {
let id = UUID()
@Published var departure: Station?
init() { print("(type(of: self)) (#function) (id.uuidString)") }
deinit { print("(type(of: self)) (#function) (id.uuidString)") }
}
struct Feature1View: View {
@StateObject private var feature1ViewModel: Feature1ViewModel
init(feature1ViewModel: Feature1ViewModel = Feature1ViewModel()) {
self._feature1ViewModel = StateObject(wrappedValue: feature1ViewModel)
print("(type(of: self)) (#function)")
}
var body: some View {
VStack {
Text("Feature1")
Text("departure (feature1ViewModel.departure?.name ?? "")")
Button {
feature1ViewModel.departure = Station(name: "Lebak Bulus Grab")
} label: {
Text("set departure")
}
NavigationLink(value: Feature1ViewRoute.Feature1_1) {
Text("Go to Feature1_1")
}
}
.navigationDestination(for: Feature1ViewRoute.self) { route in
switch route {
case .Feature1_1:
Feature1_1View()
}
}
}
}
Feature1_1View.swift
enum Feature1_1ViewRoute {
case Feature1_1_1
}
class Feature1_1ViewModel: ObservableObject {
let id = UUID()
init() { print("(type(of: self)) (#function) (id.uuidString)") }
deinit { print("(type(of: self)) (#function) (id.uuidString)") }
}
struct Feature1_1View: View {
@StateObject private var feature1_1ViewModel: Feature1_1ViewModel = Feature1_1ViewModel()
init() {
print("(type(of: self)) (#function)")
}
var body: some View {
VStack {
Text("Feature1_1View")
NavigationLink(value: Feature1_1ViewRoute.Feature1_1_1) {
Text("Go to Feature1_1_1")
}
}
.navigationDestination(for: Feature1_1ViewRoute.self) { route in
Feature1_1_1View()
}
}
}
Feature1_1_1View.swift
class Feature1_1_1ViewModel: ObservableObject {
let id = UUID()
init() { print("(type(of: self)) (#function) (id.uuidString)") }
deinit { print("(type(of: self)) (#function) (id.uuidString)") }
}
struct Feature1_1_1View: View {
@StateObject private var feature1_1_1ViewModel: Feature1_1_1ViewModel = Feature1_1_1ViewModel()
init() {
print("(type(of: self)) (#function)")
}
var body: some View {
VStack {
Text("Feature1_1_1View")
}
}
}
2
Answers
Your issue is likely attributable to SwiftUI’s initialization behavior. The navigation stack will recreate the views, which encompasses initializing the
@StateObject
s each instance.One resolution to this is to utilize an
@EnvironmentObject
as opposed to an@StateObject
. This will inhibit the ViewModel from being re-initialized every occasion the view is recreated.First, upon the app starting, initialize your ViewModel and pass it to the ContentView via
.environmentObject()
modifier:Then, inside your views, you can refer to the ViewModel:
In this scenario, the ViewModel will not be reinitialized when the Feature1View is recreated. Additionally, since the ViewModel is being created and passed in at the root of the application, it will not be annihilated until the app itself is destroyed. So this should fulfill your necessity to initialize only once.
One note on this approach: This is essentially creating a single instance of the ViewModel for the entire lifecycle of the application. It’s a bit akin to using a singleton, and it can have similar pros and cons. The ViewModel’s state will persist between navigations, which can be useful, but it also signifies that if you need to reset the ViewModel’s state when you navigate away from a view, you’ll need to do so manually.
If you want to make the ViewModel initialization and destruction more explicit, you could contemplate using an architecture pattern like MVVM, which would permit you to manage the ViewModel’s lifecycle in a more granular way. With MVVM, each view has its own ViewModel, and the ViewModel is initialized when the view is created and destroyed when the view is dismantled.
In SwiftUI, a
@StateObject
is intended to be initialized once and then to uphold its state across re-renders of the view. However, if you’re noticing your@StateObject
being initialized multiple times, it’s possible that the parent view itself is getting re-rendered, provoking all of its body to be re-evaluated and consequently theStateObject
to be re-initialized.You can resolve this issue by delivering your
@StateObject
through the view hierarchy, conceiving it at the root and conveying it as a@ObservedObject
or@EnvironmentObject
to offspring views.In your case, I would recommend initializing your view models as
@StateObject
within theContentView
and dispatch them as@ObservedObject
to the corresponding feature views.Here’s an example of how you can accomplish this:
The same pattern can be employed to the other feature views, as well.
Executing it this way certifies that each view model is initialized only once within
ContentView
and can be passed through to the feature views without being re-initialized. This approach also carries the advantage of making your state more predictable and easier to administer, since all state is governed at the top level of your application.If your app structure is complex and you find it hard to control state and relay down the necessary objects from the root of your app, you might want to contemplate using a state management library or an architectural pattern that assists with this, such as Redux or Flux.