read

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.


Image

@samwize

¯\_(ツ)_/¯

Back to Home