21

How to use .navigationDestination with Button in SwiftUI for complex navigation scenarios?

I'm currently working on a settings page which has approximately 10 buttons. Each of these buttons is not in a ForEach or List due to the need for a custom design, and each button needs to navigate to a different view with distinct variables being passed between them.

In the past, I would have used NavigationLink(destination:label) to achieve this, and while this method still works, I've run into an issue. In one or more of those views, I need to use NavigationLink(isActive), which is now deprecated.

I discovered a potentially better solution using .navigationDestination, but I'm having trouble making it work as expected. For instance, I found that it doesn't programmatically dismiss the view when it's finished being called if it's used in NavigationLink subviews.

Most of the SwiftUI tutorials and resources I've found online demonstrate navigation using List and ForEach with programmatically generated data, but not simple button clicks like in my scenario. And I have watched countless videos and even Apple documentation and do not understand anything to do with this type of new navigation.

Could someone explain how to properly use .navigationDestination and its requirements in this context? Specifically, how can I use it with Button(action:label) to navigate to the next view, and how does it behave in NavigationLink subviews?

Here's a simplified example of what I'm trying to accomplish:

First View:

NavigationStack() {
    NavigationLink(label: {
        Text("Navigate To Second View")
    }, destination: {
        SecondView()
    })
}

Second View:

Button(action: {
    isPresented = true //Goes to third view
}) {
    Text("Navigate to third view")
}
.navigationDestination(isPresented: $isPresented, destination: ThirdView())

ThirdView:

Button(action: {
    isPresented = false //Goes back to second view, however, this seems not to work
}) {
    Text("Navigate Back to second view")
}
1

2 Answers 2

13

Using Eilons answer, I was able to successfully make it work. However, it caused this error:

Only root-level navigation destinations are effective for a navigation stack with a homogeneous path.

Making some adjustments, and having a headache over this for a few days, I was finally able to come up with an answer.

First, I got rid of the enum as it is redundant to what I did instead. I first added a @State private var path = NavigationPath() instead of the @State private var navigationPath: [Route] = [] array.

Here is the full code:

struct View1: View {
    @State private var path = NavigationPath()
    
    var body: some View {
        NavigationStack(path: $path) {
            Button("Go to view 2") {
                path.append("View2")
            }
            .background(Color.red)
            .navigationDestination(for: String.self) { route in
                switch route {
                case "View2":
                    View2(path: $path)
                case "View3":
                    View3(path: $path)
                }
            }
        }
    }
}

struct View2: View {
    @Binding var path: NavigationPath
    
    var body: some View {
        Button("Go to view 3") {
            path.append("View3")
        }
        .background(Color.orange)
    }
}

struct View3: View {
    @Environment(\.dismiss) private var dismiss
    @Binding var path: NavigationPath
    
    var body: some View {
        Button("Pop view") {
            path.removeLast()
            
            // or - call `dismiss()`
            // dismiss()
        }
        .background(Color.green)
    }
}

I hope this helps someone as I had a headache over this for days. As someone who always used NavigationView & NavigationLink, switching to this was supposed to be easy, but turned out to be a pain.

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

4 Comments

to pop back to root, you can use path = [] in replace of path.removeLast()
@paranormaldist Im not sure how you got path = [] to pop to root, but what worked for me is path.removeLast(path.count)
I've been struggling with many complex examples (using List - like I have some infinite number of pages in the top level of my App). The title states "SwiftUI for complex navigation scenarios" yet - this is for SIMPLE navigation scenarios. Which is what I start with in development. Thanks James!
Ah... the secret is in the switch embedded in the nav destination modifier! Nice. and here I was trying to pass around View structs - D'ho!
4

You should use NavigationPath.

enum Route {
    case view2
    case view3
}

struct View1: View {
    @State private var navigationPath: [Route] = []
    
    var body: some View {
        NavigationStack(path: $navigationPath) {
            Button("Go to view 2") {
                navigationPath.append(.view2)
            }
            .background(Color.red)
            .navigationDestination(for: Route.self) { route in
                switch route {
                case .view2:
                    View2(navigationPath: $navigationPath)
                case .view3:
                    View3(navigationPath: $navigationPath)
                }
            }
        }
    }
}

struct View2: View {
    @Binding var navigationPath: [Route]
    
    var body: some View {
        Button("Go to view 3") {
            navigationPath.append(.view3)
        }
        .background(Color.orange)
    }
}

struct View3: View {
    @Environment(\.dismiss) private var dismiss
    @Binding var navigationPath: [Route]
    
    var body: some View {
        Button("Pop view") {
            navigationPath.removeLast()
            
            // or - call `dismiss()`
            // dismiss()
        }
        .background(Color.green)
    }
}

4 Comments

You can declare another navigationDestination in View2 to remove the need for the enum and switch in View1
Can you explain more of this?
And I need some of the child sub views to have other bindings from parent sub views that are unknown to the "View1". I am unable to successfully define this if the views are managed in the first view
A good solution. An alternative to a Binding is to simply use a closure in each destination view, which can set navigationPath = [] when called. Can make for a very clean approach.

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.