7

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")
    }
}
Jeff
  • 3,829
  • 1
  • 31
  • 49
  • Having the same issue. Seems to be a problem with multiple .navigationDestination modifiers however I was under the impression that's how we were supposed to do things going forward... – Kudit Dec 22 '22 at 20:04
  • Status 2022-12-24: a month ago I used code-level support and opened a Technical Support Incident (TSI). The tech told me to open a Feedback Assistant ticket to get the issue in front of "the proper SwiftUI engineer for further investigation." I reported the Feedback ID to the TSI tech. After that, my ticket disappeared from my items in Feedback Assistant. No further contact from Apple for 2 weeks. I surmise that this is a defect. For now, use whatever workaround you can concoct. – Jeff Dec 24 '22 at 17:31
  • @Jeff maybe they listened. It looks like there's a fix in iOS 16.4. https://developer.apple.com/documentation/ios-ipados-release-notes/ios-ipados-16_4-release-notes#SwiftUI-Navigation – Ryan Mar 06 '23 at 03:51
  • I have a similar infinite loop problem that I have been trying to figure out for months. I created a very simple app to demonstrate and submitted a Technical Support Incident (TSI). Apple replied but that wasn't helpful at all. I did more investigation and I found that it seems to be related to using a binding with navigationDestination. I was excited to see a fix night be in iOS 16.4 but I just downloaded Xcode 14.3 beta and ran it on a simulator running iOS 16.4 and sadly it did not fix my infinite loop problem. – Jeff Zacharias Mar 19 '23 at 18:52
  • See this answer https://stackoverflow.com/a/76230910/8617744 – Md. Yamin Mollah May 11 '23 at 19:09

2 Answers2

4

Perhaps I'm a bit late to the party, but I recently had the same issue in a project where each view has a ViewModel object that I was using in my .navigationDestination(...). Caused an infinite loop sometimes, not every time. The rule we have introduced is to not reference any object that the view is dependant on in the .navigationDestination(...)-function. Seems to work after this rule was applied.

This did not work:

@StateObject var viewModel: ViewModel

var body: some View {
    Text("Some View")
        .navigationDestination(for: Routing.self) { route in
            switch route {
                case .showMap:
                    MapView(viewModel.someValue)
            }
        }
}

This seems to work:

@StateObject var viewModel: ViewModel

var body: some View {
    Text("Some View")
        .navigationDestination(for: Routing.self) { route in
            switch route {
                case .showMap(let someValue):
                    MapView(someValue)
            }
        }
}
Albin
  • 105
  • 9
  • This answer was super helpful! This got me unstuck slightly with my problem of infinite loops. Unfortunately I can't figure out how to get bindings to work from my `navigationDestination`. i.e. I can pass data _into_ the destination, but I can't figure out how to surface data back to the parent component. I can't pass bindings in and can't pass functions in. Any advice @Albin on how to surface data back from the destination? – MrGrinst Jan 30 '23 at 02:24
  • I'm glad it helped! That is a good question. In my case I'm using a Dependency Injection framework to inject e.g. a state-object inte my view model classes that I can use to communicate between view models. (The DI framework I use is Resolver but Factory seems to be the recommended framework nowadays, if you're interested https://github.com/hmlongco/Factory). Although I can't imagine that Apple did not intend for developers to use bindings in a .navigationDestination, that seems super buggy... Hopefully they will patch this soon. – Albin Jan 30 '23 at 09:58
  • See this answer https://stackoverflow.com/a/76230910/8617744 – Md. Yamin Mollah May 11 '23 at 19:10
  • @MrGrinst, see my answer about using a closure capture list, and the comment regarding bindings. Maybe that helps in your situation? – ecp May 13 '23 at 23:23
1

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)
}

Why this works

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.

ecp
  • 323
  • 3
  • 7