Programmatically navigating nested NavigationLinks

In the app I’m working on I’m trying to navigate to the second level of a navigation link chain from elsewhere in the app. To do that I’m attempting to utilize tag/selection to specify where I want to navigate.
The code you can see below is an example project that shows the weird bugs that arise.

import SwiftUI

@main
struct NavigationLinkTestingApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
                .environmentObject(ViewModel())
        }
    }
}

class ViewModel: ObservableObject {
    @Published var selection: String? = nil
}

struct ContentView: View {
    
    @EnvironmentObject var model: ViewModel
    
    var body: some View {
        
        NavigationView {
            VStack(spacing: 30) {
                
                NavigationLink(tag: "screen2", selection: $model.selection) {
                    Screen2()
                } label: {
                    EmptyView()
                }
                .isDetailLink(false)
                
                Button("Go to Screen 2") {
                    model.selection = "screen2"
                }
                
                Button("Go to screen 3") {
                    model.selection = "link3"
                }

            }
            .navigationTitle("This is screen 1")
        }
    }
}

struct Screen2: View {
    
    @EnvironmentObject var model: ViewModel
    
    var body: some View {
        VStack(spacing: 30) {
            
            NavigationLink(tag: "screen3", selection: $model.selection) {
                Screen3()
            } label: {
                EmptyView()
            }
            .isDetailLink(false)
            
            Button("Go to Screen 3") {
                model.selection = "screen3"
            }

        }
        .navigationTitle("This is screen 2")
    }
}

struct Screen3: View {
    
    @EnvironmentObject var model: ViewModel
    
    var body: some View {
        Text("This is screen 3")
    }
}

The two bugs I noticed when running this are:

  1. The “Go to screen 3” button at the top level does nothing even though it just dose change the value in the ViewModel.
  2. Navigating to screen3 from screen2 only holds the view for a split second before popping back out to the top level.

Is there a way to fix this? Is this even the best way to go about solving this navigation problem?

Hi @Nemo_of_Ithaca, I have checked your code, and I observed that you’re using tag and selection value to navigate from screen 1 to screen 2, and screen 2 to screen 3.

The problem that I found is that in screen 1, you used the viewModel.selection as binding to the navigation link to check for the “screen2” tag, which in turn navigates to screen2.

In your screen 2, you used the same viewModel.selection binding to check for “screen3” to navigate to Screen3, but you forgot that if you change that, it would also affect the binding that you had in screen 1. So if you change viewModel.selection to “screen3”, the binding that you had in screen1 would also be affected, which is why it disables the navigation link that presented screen2 and therefore brings you back to screen 1.

As an alternative, you may want to use different variables to hold the selection of each screen. If you want to navigate 2 screens deep programmatically, I think you can use the isActive parameter instead of tag and selection. basically what you want is to have two boolean variables in your viewmodel that would set the navigation isActive values for screen 2 and 3.

so if you want to navigate to screen 2, set the isActive parameter to true for screen2, and false for screen 3.

if you want to navigate to screen 3 via screen 2, you need to set isActive parameters to true to both screen2 and screen3.

for guidance on how to use isActive parameter, check out the official docs:
https://developer.apple.com/documentation/swiftui/navigationlink

I also found this video on youtube from Rebeloper about his paid solution to the navigation problem of swiftui. You can also check it if you want.

PLEASE NOTE
As a fellow learner of SwiftUI, I want to tell you that this may not a good approach on how to do programmatic navigation.

One reason being is that when you put all isActive binding variables in a shared view model, managing the navigation would be difficult to scale. basically, you will have a lot of switches inside your shared view model that may affect the navigation flow and would be pretty hard to trace if something goes wrong deep in the navigation just like the bug that you have described in your question.

I defiantly agree that this does not feel like a good approach. Being new to Swift sometimes I don’t know whether or not this is just a difficult problem or I’m going about it the wrong way and making it hard on myself.
Your solution fixed one of my bugs but I still can’t navigate to screen3 from screen1.
Although the video helped me quite a bit simply by providing the vocab word “deeplinks”, hopefully with this more specific search term I’ll be able to find a solution.

You don’t have a NavigationLink on that screen that is tied to screen3. You have one for screen2, but you need one that leads to screen3 if you want to go there from screen1 (like you do in Screen2).

Consider posting your question on Stackoverflow…