Melodius
Melodius

Reputation: 2775

Problem with bindings not updating correctly in SwiftUI

I have the following minimal example (Storage has been injected into the environment in @main):

import SwiftUI

struct Model {
    var value: Int = 0
}

class Storage: ObservableObject {
    @Published var model = Model()
}

struct ContentView: View {
    var body: some View {
        NavigationView {
            VStack {
                Text("Test app")
                NavigationLink(
                    destination: SubView(),
                    label: {
                        Text("SubView")
                    })
            }
        }
    }
}

struct SubView: View {
    @EnvironmentObject var storage: Storage
    
    var body: some View {
        VStack {
            Text("Seconds: \(storage.model.value)")
            NavigationLink(
                destination: SubSubView(value: $storage.model.value),
                label: {
                    Text("SubSubView")
                })
        }
    }
}

struct SubSubView: View {
    @Binding var value: Int
    
    var body: some View {
        VStack {
            Text("Seconds: \(value)")
            NavigationLink(
                destination: TimerView(value: $value),
                label: {
                    Text("TimerView")
                })
        }
    }
}

struct TimerView: View {
    @Binding var value: Int

    var body: some View {
        VStack {
            Text("Seconds: \(value)")
            Button("Add 300 s") {
                value += 300
            }
        }
    }
}

When you navigate down to the TimerView and press the button to add 300 seconds to the binded variable, the displayed value does not update. If you navigate back to SubView, it will then display the changed value, indicating that the model has been correctly updated even though the UI did not update synchronously.

If I replace ContentView with SubView in app startup and add the NavigationView to SubView, then the binded value changes correctly in the UI in TimerView and SubSubView. What gives?

In case someone asks, "why are you passing a binding to sub sub views?", the answer is that the Model I'm using has 21 variables and I want to use the same TimerView to alter any one of those depending on what the user has selected, instead of creating 21 TimerView:s to handle every variable in Model.

Upvotes: 2

Views: 4302

Answers (1)

Asperi
Asperi

Reputation: 257493

Passing Binding too deeply is unreliable (and I consider as bad practice). Think about Binding as one-to-one view link, not more.

So the solution for your case would be pass view model in navigation hierarchy and use binding only locally, like

struct SubView: View {
    @EnvironmentObject var storage: Storage
    
    var body: some View {
        VStack {
            Text("Seconds: \(storage.model.value)")
            NavigationLink(
                destination: SubSubView(),    // << just navigate !!
                label: {
                    Text("SubSubView")
                })
        }
    }
}

struct SubSubView: View {
    @EnvironmentObject var storage: Storage
    
    var body: some View {
        VStack {
            Text("Seconds: \(storage.model.value)")
            NavigationLink(
                // I would pass observable view model even into `TimerView`
                destination: TimerView(value: $storage.model.value),
                label: {
                    Text("TimerView")
                })
        }
    }
}

Upvotes: 4

Related Questions