I am executing a SwiftUI playground that contains 2 labels and 2 buttons that modified the value of these labels.
I’ve stored the value of these labels in a @ObservableObject
. Whene I modify the value of any of these properties, both views CustomText2
and CustomText3
are reinitialized, even the one that his values has not changed.
Code:
final class ViewModel: ObservableObject {
@Published var title: Int
@Published var title2: Int
init(title: Int = 0, title2: Int = 0) {
self.title = title
self.title2 = title2
}
}
struct ContentView: View {
@StateObject var viewModel = ViewModel()
var body: some View {
VStack {
Button(
action: {
viewModel.title += 1
}, label: {
Image(systemName: "globe")
.imageScale(.large)
.foregroundColor(.accentColor)
}
)
CustomText1(
title: $viewModel.title
)
Button(
action: {
viewModel.title2 += 1
}, label: {
Image(systemName: "globe")
.imageScale(.large)
.foregroundColor(.accentColor)
}
)
CustomText2(
title: $viewModel.title2
)
}
.padding()
}
}
struct CustomText1: View {
@Binding var title: Int
init(
title: Binding<Int>
) {
self._title = title
}
var body: some View {
Text("(title)")
.foregroundColor(.black)
}
}
However if I store both properties as @State
in the view and I modify them, the CustomTexts
are not reinitialized, they just update their value in the body without executing an init.
Why are they getting reinitialized when I store both properties in the ViewModel
?
I’ve tried to make the views conforming Equatable but they’re reinitialized.
Can be a performance problem if the views are initialized many times?
I am interested in not having the subviews reinitialized because I want to perform custom stuff in the init of some subviews.
2
Answers
@ObservableObject is for model data, not view data.
The reason is when using lets or @State vars, SwiftUI uses dependency tracking to decide if body needs to be called and in your case body doesn’t use the values anywhere so there is no need to call it.
It can’t track objects in the same way, if there is a @StateObject declared then body is called regardless if any properties are accessed, so it’s best to start with @State value types and only change to @StateObject when you really need features of a reference type. Not very often now we have .task which is the place to put your custom async work.
When you have one StateObject that encompasses multiple State variables, change in one will redraw the entire view. In your case, any change in any variable in viewModel will trigger the publisher of viewModel and reload ContentView
Also we are not supposed to make any assumptions on when a View will be redrawn, as this might change with different versions of SwiftUI. Its better to move this custom stuff you are doing in the init of views to some other place(if it can be). Init should only do work needed to redraw the view with the new state parameters and nothing else.