Reputation: 2775
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
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