9

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")
    }
}
7
  • 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... Commented Dec 22, 2022 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. Commented Dec 24, 2022 at 17:31
  • @Jeff maybe they listened. It looks like there's a fix in iOS 16.4. developer.apple.com/documentation/ios-ipados-release-notes/… Commented Mar 6, 2023 at 3:51
  • 2
    For folks finding this issue the way I did, just wanted to note that this issue seems to occur on Xcodes 15.0, 15.0.1, and 15.1 (no additional releases at time of writing) and only on iOS simulators OR devices running iOS 16.0 up to and including 16.3. I did not test iOS versions earlier than 16.0, but we did test every version from 16.0 and later across Xcode 14.3 and later. Commented Dec 14, 2023 at 21:32
  • 1
    Same is the case with me... working fine with devices running iOS 17 and + but freezing the device iPhone x running iOS 16.7. Commented Feb 6, 2024 at 11:14

2 Answers 2

10

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.

Sign up to request clarification or add additional context in comments.

Comments

7

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

5 Comments

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?
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 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.
@MrGrinst, see my answer about using a closure capture list, and the comment regarding bindings. Maybe that helps in your situation?
This was extremely helpful!

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.