Observation's @State init multiple times

Here’s a minimal code sample that reproduces the bug.

@main
struct ObservationApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}

struct ContentView: View {
    @State var observableOne = ObservableOne()
    init() {
        print("ContentView init")
    }
    var body: some View {
        Button("Button") {
            print("> Button pressed")
            observableOne.observableTwo.addItem()
        }
    }
}

@Observable
class ObservableOne {
    let observableTwo = ObservableTwo()
    init() {
        print("ObservableOne init")
        withObservationTracking {
            _ = observableTwo.items
        } onChange: {
            // empty
        }
    }
}

@Observable
class ObservableTwo {
    var items: [String] = []
    func addItem() {
        self.items.append("New")
    }
}

Console output:

ObservableOne init
ContentView init
Button pressed
ObservableOne init
ContentView init

As I understand it, there should only be one init call for ObservableOne.
Additional info: XCode 16.2, Swift 6

Every time ContentView is initialized, there will be a new instance of ObservableOne created. By the time ContentView.body is run, SwiftUI will have thrown it away and replaced it with the original.

You are starting observation tracking inside the body of ObservationApp, so SwiftUI thinks that you're actually using it there, and thus it has to invalidate the body of that, as well, which leads to two ContentViews being created. If you delete the init of ObservableOne, the problem will go away.

For additional context, StateObject, arguably the precursor to using State with something that could be observed, lazily created the state it was wrapping. I don't know that Apple has ever said why they moved away from this with the introduction of using State + Observation.

1 Like

This behavior is briefly discussed in the documentation for SwiftUI.State:

A State property always instantiates its default value when SwiftUI instantiates the view. For this reason, avoid side effects and performance-intensive work when initializing the default value.

The implication is that creating a view component N times will lead to N default values… but only the first default value is "saved" across the lifecycle of the component.

1 Like