Search code examples
swiftuibindingstateobservableobjectproperty-wrapper-published

SwiftUI state management


I have encountered an architectural problem with SwiftUI state.

Consider an architecture with a uni-directional data flow.

class Store: ObservableObject {
    @Published private(set) var state: AppState = AppState(initialValue: 0)

    func fetchInitialValue() async {
        try? await Task.sleep(for: .seconds(1))
        state = AppState(initialValue: 5)
    }
}

struct AppState {
    let initialValue: Int
}

We have an ObservableObject - Store, which has a @Published variable State, which can only be updated internally. This state contains an initial value for some view, which can be updated after the view initially presented (e.g. it can be fetched from some remote system).

Now, this initialValue should be only an initial value for that view, it shouldn't be a two-way binding, but whenever it is updated, the view should update.

Here is an example view hierarchy using this state. Note, that the view can change the value on its own, which should not affect the initialValue.

struct MainView: View {
    @ObservedObject var store = Store()
    var body: some View {
        VStack {
            Text("Initial value is \(store.state.initialValue)")

            SubView(initialValue: store.state.initialValue)
        }
        .onAppear {
            Task {
                await store.fetchInitialValue()
            }
        }
    }
}

struct SubView: View {
    @State var currentValue: Int

    init(initialValue: Int) {
        currentValue = initialValue
    }

    var body: some View {
        VStack {
            Stepper(value: $currentValue) {
                Text("Current value is \(currentValue)")
            }
        }
    }
}

The problem is that SubView will not update on the state update, because none of its subviews depend on the initialValue.

Any ideas how to force SubView to recreate on the state change?


Solution

  • View re-renders on the state change. Passing a value type like the initialValue which is an integer will not update it dependant to the store. So make it change the state. For example using the id modifier:

    SubView(initialValue: store.state.initialValue)
        .id(store.state.initialValue)
    

    or passing the store itself:

    SubView(store: store)