NavigationStack - Pushing root View twice not working

28 Views Asked by At

Perhaps I am not understanding NavigationStack well and hope someone can help me.

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?

2

There are 2 best solutions below

0
Sweeper On BEST ANSWER

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()
        }
    }
}
0
Hiren Patel On

Your SwiftUI implementation using NavigationStack, NavigationLink, and dynamic navigation destinations is mostly correct. However, there might be confusion regarding how navigation paths work, especially when trying to push views of the same type onto the navigation stack.

When you push the same view onto the stack, SwiftUI might not render the view again if it considers the new view to be the same as the current one. This is because SwiftUI performs some optimizations under the hood to prevent unnecessary re-renders. When you attempt to navigate to the same view type (ViewA in this case) with the same parameters, SwiftUI might not see it as a change.

Here's an improved approach that ensures distinct navigation experiences by using a more explicit handling of the navigation path. This approach helps SwiftUI understand that you want to navigate to a new instance of the view even if it's of the same type.

Updated ViewA and ViewB For ViewA, you can use an explicit identifier for each view instance to ensure SwiftUI treats each navigation as unique.

struct ViewA: View {
let someText: String?
let someNumber: Int?
let id = UUID() // Unique identifier for each ViewA instance

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

}

For ViewB, you don't need to change much, but ensure you are providing unique values or handling the navigation path appropriately.

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

}