I'm trying to optimize my SwiftUI app. I have a strange behavior with a ViewModel stored as a @StateObject in its View. To understand the issue, I made a small project that reproduces it.
ContentView contains a button to open ChildView in a sheet. ChildView is stored as property as I don't want to recreate it every time the sheet is open by user (this works):
struct ContentView: View {
@State private var displayingChildView = false
private let childView = ChildView()
var body: some View {
Button(action: {
displayingChildView.toggle()
}, label: {
Text("Display child view")
})
.sheet(isPresented: $displayingChildView, content: {
childView // instead of: ChildView()
})
}
}
ChildView code:
struct ChildView: View {
@StateObject private var viewModel = ViewModel()
init() {
print("init() of ChildView")
}
var body: some View {
VStack {
Button(action: {
viewModel.add()
}, label: {
Text("Add 1 to count")
})
Text("Count: \(viewModel.count)")
}
}
}
And its ViewModel:
class ViewModel: ObservableObject {
@Published private(set) var count = 0
init() {
print("init() of ViewModel")
}
func add() {
count += 1
}
}
Here is the issue:
The ViewModel's init is called every time user opens the sheet. Why?
As ViewModel is a @StateObject in ChildView and ChildView is only init once, I am expecting that ViewModel is also only init once.
I have read this article that says that :
Observed objects marked with the @StateObject property wrapper don’t get destroyed and re-instantiated at times their containing view struct redraws.
Or here:
Use @StateObject once for each observable object you use, in whichever part of your code is responsible for creating it.
So I understand that ViewModel should stay alive, especially as ChildView is not destroyed.
And what confuses me the most is that if I replace @StateObject with @ObservedObject it works as expected. But it is not recommended to store an @ObservedObject inside a View.
Can anyone explain why this behavior and how to fix it as expected (ViewModel init should be called once) ?
A possible solution:
I've found a possible solution to fix this behavior:
a. Move the declaration of ViewModel into ContentView:
@StateObject private var viewModel = ViewModel()
b. Change the declaration of ViewModel in ChildView to be an EnvironmentObject:
@EnvironmentObject private var viewModel: ViewModel
c. And inject it in childView:
childView
.environmentObject(viewModel)
That means it's ContentView that is responsible to keep the ChildView's ViewModel alive. It works, but I find this solution quite ugly:
- All future child Views of
ChildViewcould get access toViewModelthrough environment objects. But it's no sense as it should be only useful for its View. - I would prefer declare a ViewModel inside its View instead of inside its parent View.
And this solution still doesn't explain above questions about @StateObject that should stay alive...
Viewstruct and@Stateis for.