swiftuiswiftui-navigationstack

NavigationStack - Pushing root View twice not working


Perhaps I am not understanding NavigationStack well.

I've setup a small example to demonstrate the issue:

@main
struct StacksApp: App {
    var body: some Scene {
        WindowGroup {
            ViewA(someText: "Root View")
        }
    }
}

ViewA (AKA Root View)

struct ViewA: View {
    private let someText: String?
    private let someNumber: Int?

    @State private var navigationPath = NavigationPath()

    init(someText: String? = nil,
         someNumber: Int? = nil) {
        self.someText = someText
        self.someNumber = someNumber
    }

    var body: some View {
        NavigationStack(path: $navigationPath) {
            VStack(spacing: 10) {
                if let someText {
                    Text("Showing \(someText) text")
                } else if let someNumber {
                    Text("Showing \(someNumber) number")
                }

                NavigationLink("Show View B", value: "ViewB")
            }
            .navigationDestination(for: String.self) { value in
                if value == "Hello" {
                    ViewA(someText: value)
                } else {
                    ViewB(path: $navigationPath)
                }
            }
            .navigationDestination(for: Int.self) { value in
                ViewA(someNumber: value)
            }
        }
    }
}

ViewB

struct ViewB: View {
    @Binding var path: NavigationPath

    var body: some View {
        VStack(spacing: 10) {
            Text("Hello, World!, this is View B!")
            NavigationLink("Go to ViewA - Showing Int value", value: 4)
            NavigationLink("Go to viewA Showing text Value", value: "Hello")
        }
        .navigationDestination(for: Int.self) { value in
            ViewA(someNumber: value)
        }
    }
}

The Problem

From ViewB I am unable to go to ViewA with different data - eg an Int. When I tap on "Go to ViewA - Showing Int value" or "Go to ViewA - Showing text value"

What happens is a new View is pushed on to the stack and then it pops me back to the original ViewA: ViewA(someText: "Root View")

If I were to replace ViewA with some other view that I was to show multiple times, with different data - eg ViewB - there is no issue and it works as expected.

Is this by design? Eg pushing a root view multiple times not being allowed?


Solution

  • The problem is that the NavigationStack is part of ViewA. When you navigate to another ViewA, you get a new NavigationStack that is nested in the existing one. This causes the problems you described

    ViewA should not contain a NavigationStack - it should just be the view you put inside the NavigationStack { ... }.

    struct ViewA: View {
        let someText: String?
        let someNumber: Int?
        
        @Binding var navigationPath: NavigationPath
        
        init(someText: String? = nil, someNumber: Int? = nil, navigationPath: Binding<NavigationPath>) {
            self.someText = someText
            self.someNumber = someNumber
            self._navigationPath = navigationPath
        }
        
        var body: some View {
            VStack(spacing: 10) {
                if let someText {
                    Text("Showing \(someText) text")
                } else if let someNumber {
                    Text("Showing \(someNumber) number")
                }
                
                NavigationLink("Show View B", value: "ViewB")
            }
        }
    }
    

    The NavigationStack should go in another view. Note that the navigation destinations should also go here, otherwise you would end up having multiple navigation destinations for the same type, when ViewA is pushed for the second time.

    struct ContentView: View {
        @State var navigationPath = NavigationPath()
        var body: some View {
            NavigationStack(path: $navigationPath) {
                ViewA(someText: "Foo", navigationPath: $navigationPath)
                    .navigationDestination(for: String.self) { value in
                        if value == "Hello" {
                            ViewA(someText: value, navigationPath: $navigationPath)
                        } else {
                            ViewB(path: $navigationPath)
                        }
                    }
                    .navigationDestination(for: Int.self) { value in
                        ViewA(someNumber: value, navigationPath: $navigationPath)
                    }
            }
        }
    }
    
    @main
    struct StacksApp: App {
        var body: some Scene {
            WindowGroup {
                ContentView()
            }
        }
    }