I have read lots of other questions and answers about infinite loops in SwiftUI. My question is different, although maybe this typo question is relevant, but I do not think so.
I have narrowed the problem to this: in a NavigationStack, a lower level navigationDestination that uses a different identifiable type in the destination closure than the for data type, creates an infinite loop at the upper level navigationDestination destination closure.
I have spent several hours reducing and abstracting the recreate code. This is as condensed as I could make it. When I simplify further, the infinite loop disappears, and I cannot determine why, yet. For example, I created a single layer NavigationStack (not shown) where the destination closure does not use the for data type, but it works correctly.
struct F3: Identifiable, Hashable {
let id: String = UUID().uuidString
let t: String
}
struct R3: Identifiable, Hashable {
let id: String = UUID().uuidString
let t:String
}
struct N3: Identifiable, Hashable {
let id:String = UUID().uuidString
let t: String
}
struct LV3: View { // Use `App` conformer to load this View in WindowGroup.
let f2z = [ F3(t: "A"), F3(t: "B"),]
var body: some View {
NavigationStack {
List(f2z) { f in
NavigationLink(f.t, value: f)
}
.navigationDestination(for: F3.self) { f in
VV3() // Infinite loop here.
}
.navigationTitle("L")
}
}
}
struct VV3: View {
let r = R3(t: "rrr")
let nz: [N3] = [
N3(t: "hhh"),
N3(t: "ttt"),
]
var body: some View {
List(nz) {
NavigationLink($0.t, value: $0)
}
.navigationDestination(for: N3.self) { n in
Text(r.t) // Changing to String literal or `n.t` fixes the infinite loop.
}
.navigationTitle("V")
}
}
This can be avoided with 2 small changes. Change the let r property to a state variable:
@State var r = R3(t: "rrr")
And then add a capture closure list ([r]) for that variable in the destination closure:
.navigationDestination(for: N3.self) { [r] n in
Bonus tip: If you happened to be doing something where your destination view needed a binding (if it needed to change the state), you could indicate the binding ([$r]) in the list instead:
.navigationDestination(for: N3.self) { [$r] n in
SomeViewThatChangesTheValue($r)
}
The @State var r keeps r's value from being recreated if/when the instance of the VV3 view needs to be regenerated. And that matters because its id is the random value UUID().uuidString. When the id changes, that can trigger other changes in the View hierarchy. Apparently the changes trigger more changes in this case, resulting in an infinite loop.
And the closure capture list entry for r is needed because the state wrapper itself will get recreated if the view needs to be regenerated, and the closure capture list tells Swift to use the call-time state (wrapper) instead of the one at the time the closure was created, from the old view instance. (The exact reason why this is an issue isn't totally clear to me. I'm just speculating, but I guess just the fact that it's tied to a view that's no longer in use is enough to break things one way or another. Maybe someone with a better understanding of SwiftUI internals can clarify/correct what I'm saying here.)
You can see how the varying values affects things by going back to the original code and just changing the ids of both R3 and N3 to:
var id: String { t }
If you do that (makingid's value constant, since t is constant), the rest of the original code works. (Both id's, because the NavigationStack is using N3, and the closure is using R3.) Not that you'd necessarily want to do that, depending on what your real code does here. But just to demonstrate the issue.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With