What is the solution to this NavigationStack glitch?

114 Views Asked by At

I've encountered a strange glitch with NavigationStack when mixing the old-style NavigationLink with the new .navigationDestination style ones. While this example is trivial and easily fixed here, in my larger project, I can't convert everything at once. As a result, there are instances where both styles are used within the same NavigationStack.

The following preview demonstrates the issue: You'll tap 'One', then 'Two', expecting a transition to 'Three'. However, instead of 'Three', 'Two' reappears. This cycle continues indefinitely if 'Two' is repeatedly tapped.

When navigating back, the strange behaviour continues, and we see 'Three' appearing at each transition on the way back out.

Any ideas on this very peculiar bug?

#Preview {
    NavigationStack {
        NavigationLink("One") {
            NavigationLink("Two", value: 1)
                .navigationDestination(for: Int.self) { _ in
                    Text("Three")
                }
        }
    }
}
2

There are 2 best solutions below

0
lorem ipsum On

This seems like a bug, a report to Apple is more appropriate than a workaround.

I was going to suggest a wrapper than can quickly convert the old

NavigationLink(
    _ titleKey: LocalizedStringKey,
    @ViewBuilder destination: () -> Destination
) 

to

func navigationDestination<V>(
    isPresented: Binding<Bool>,
    @ViewBuilder destination: () -> V
) -> some View where V : View

The behavior still exists. It seems like "Two" gets pushed over "Three" every time.

I added some navigationTitle to visualize what was going on.

You can notice that the Back button switches to "Three` when clicking on Three but Two is shown in the content.

Here is the code I used.

import SwiftUI

struct NewNavigationStack: View {
    var body: some View {
        NavigationStack {
            NavigationLinkTemp("One") {
                NavigationLink("Two", value: 1)
                    .navigationTitle("Two")
                    .navigationDestination(for: Int.self) { _ in
                        Text("Three")
                            .navigationTitle("Three")
                    }
                
            }.navigationTitle("One")
        }
    }
}

#Preview {
    NewNavigationStack()
}

struct NavigationLinkTemp<Destination: View>: View {
    let titleKey: LocalizedStringKey
    let destination: Destination
    
    @State private var isPresented: Bool = false
    init(
        _ titleKey: LocalizedStringKey,
        @ViewBuilder destination: () -> Destination
    ){
        self.titleKey = titleKey
        self.destination = destination()
    }
    var body: some View {
        Button(titleKey) {
            isPresented.toggle()
        }
        .navigationDestination(isPresented: $isPresented) {
            destination
        }
    }
} 
2
nikhil swami On

i believe, a different navigation strategy can be a solution to the problem however comes with "no guarentees", example using button:

NavigationStack {
    Button("One") {
        // ssome view
    }
    .navigationDestination {
        Button("Two") {
            //  view 2
        }
        .navigationDestination {
            Text("Three")
        }
    }
}

or Use programmatic navigation with case-switch logic

enum Destination {
    case one, two, three
}

struct ContentView: View {
    @State private var path = [Destination]()
    
    var body: some View {
        NavigationStack(path: $path) {
            Button("One") {
                path.append(.one)
            }
            .navigationDestination(for: Destination.self) { destination in
                switch destination {
                case .one:
                    Button("Two") {
                        path.append(.two)
                    }
                case .two:
                    Button("Three") {
                        path.append(.three)
                    }
                case .three:
                    Text("Three")
                }
            }
        }
    }
}