There is a pitfall with initializing a State
or a StateObject
. I have provided a snippet on how to init, but it really needs to provide the following warning.
From Apple Doc,
SwiftUI only initializes a state object the first time you call its initializer in a given view. This ensures that the object provides stable storage even as the view’s inputs change. However, it might result in unexpected behavior or unwanted side effects if you explicitly initialize the state object.
This has serious implications that most developer will overlook, until you wonder why a view is not updating correctly.
Yea I just had that moment.
State shouldn’t be init externally
In fact, you shouldn’t init any state externally. This simple demo will illustrate why.
struct ChildView: View {
@State var count: Int
var body: some View {
HStack {
Button("Increment ChildView's count") { count += 1 }
Text("\(count)")
}
}
}
The child view looks fine, except that it requires the count state to be init by the parent.
struct ParentView: View {
@State var parentCount: Int = 1
var body: some View {
VStack {
ChildView(count: parentCount) // 🤔 Would ChildView update when parentCount is changed?
Button("Increment parentCount") { parentCount += 1 }
}
}
}
The parent view also has a button that increment it’s own count (parentCount
, also a State). The parent creates ChildView with the parentCount
. The question is:
Would ChildView update when parentCount is changed?
The answer is surprisingly no.
The child state is initialized once only (starts with 1), and it will use that same state throughout it’s lifetime. That means, even if the parent changes parentCount, ChildView will still use the very first state.
How should you init state?
The only time you should init your state is inline, like how ParentView did.
@State private var parentCount: Int = 1
Furthermore, @State
should be private and shouldn’t be exposed. Adding the private
access level is good practice, which will also prevent external initialization.
Avoid using State(initialValue:)
and StateObject(wrappedValue:)
too.
Quick fix with id
That said, if you still want to misuse State, you can force the view to update with a simple trick.
ChildView(count: parentCount)
.id(parentCount)
This is legit, and is mentioned in Apple Doc. Just note the side effects.
Other solutions
Or use Binding
properly.
Or use ObservedObject
which has custom lifetime. You can init with ObservedObject(wrappedValue:)
, and unlike State, it won’t be initialized once only.