Search code examples
swiftuiswiftui-navigationlinkswiftui-navigationstack

How to make NavigationStack work with Views and popToRoot in SwiftUI?


I'm trying to use NavigationStack with Views in SwiftUI, but I'm having trouble understanding its functionality. I have gone through several tutorials, but I still don't understand the purpose of the value parameter and why all the tutorials use ForEach to create subviews instead of linking actual views.

In my case, I’m just trying to make the following code work and go back to the ContentView() when I press the “Go to ContentView” button, but nothing happens and the rootPath.count remains at 0:

-----------------ContentView-----------------
struct ContentView: View {
    @State private var path = NavigationPath()
    
    var body: some View {
        NavigationStack(path: $path) {
            Image(systemName: "figure.fall")
                .imageScale(.large)         .foregroundStyle(.tint)
            NavigationLink("Go to FirstView") {
                FirstView(rootPath: $path)
            }
        }
        .padding()
    }
}

------------------FirstView------------------
struct FirstView: View {
    @Binding var rootPath: NavigationPath
    
    var body: some View {
        NavigationLink("Go to SecondView") {
            SecondView(rootPath: $rootPath)
        }
    }
}

-----------------SecondView------------------
struct SecondView: View {
    @Binding var rootPath: NavigationPath
    
    var body: some View {
        NavigationLink("Go to ThirdView") {
            ThirdView(rootPath: $rootPath)
        }
    }
}

------------------ThirdView------------------
struct ThirdView: View {
    @Binding var rootPath: NavigationPath
    
    var body: some View {
        Text("RootPath: \(rootPath.count)")
        Button(action: {
            rootPath = NavigationPath()
        }, label: {
            Text("Go to ContentView")
        })
    }
}

Solution

    1. Each view you want to put on the stack (first, second, third) should have an associated value, which can be anything that is Hashable. Making a new type for it might be a bit clearer for what you are trying to do here. For example:
    enum Destination: Hashable { case first, second, third }
    

    You associate the value to the view when navigationDestination gets called. The root view doesn't need a value.

    1. The path is just a representation of the stack as a collection of values (which represent views). Don't use NavigationPath type (it's only for cases where your view value types can be different), instead almost always just an array will do. You update the path by NavigationLink or alternatively just assigning/appending/removing something to it directly. The path is on top of the root, so making it empty gets you back all the way.
    var path: [Destination] = []`
    

    Here's your code modified with the points above, it might clarify things a bit.

    class PathState: ObservableObject {
      enum Destination: String, Hashable {
        case first,second,third
      }
      @Published var path: [Destination] = []
    }
    
    //-----------------ContentView-----------------
    struct ContentView: View {
      @StateObject var pathState = PathState()
      var body: some View {
        NavigationStack(path: $pathState.path) {
          Image(systemName: "figure.fall")
            .imageScale(.large)
            .foregroundStyle(.tint)
            .navigationDestination(for: PathState.Destination.self) { destination in
              switch destination {
              case .first:
                FirstView()
              case .second:
                SecondView()
              case .third:
                ThirdView()
              }
            }
          NavigationLink("Go to FirstView", value: PathState.Destination.first)
        }
        .padding()
        .overlay(alignment: .top) {
          Text("Navigation Path: \(pathState.path.map(\.rawValue).debugDescription)")
        }
        .environmentObject(pathState)
      }
    }
    
    //------------------FirstView------------------
    struct FirstView: View {
      var body: some View {
        NavigationLink("Go to SecondView", value: PathState.Destination.second)
      }
    }
    
    //-----------------SecondView------------------
    struct SecondView: View {
      var body: some View {
        NavigationLink("Go to ThirdView", value: PathState.Destination.third)
      }
    }
    
    //------------------ThirdView------------------
    struct ThirdView: View {
      @EnvironmentObject var pathState: PathState
      var body: some View {
        Text("RootPath: \(pathState.path.count)")
        Button(action: {
          pathState.path = [] // take everything off the navigation stack
        }, label: {
          Text("Go to ContentView")
        })
      }
    }
    

    More info about navigationDestination:

    As you make new navigation screens add them to the enum. If you want to define them all up front, you can just add a default to the switch to catch the cases where the view hasn't been built yet, like default: Text("Under Construction").

    Now, you don't need to use a custom enum for the path value or make a PathState object at all, that is just one way to organize things and makes sense for this example. The navigationDestination just needs to return some view for a given navigation value that is being pushed. It can always be the same general view. Like:

    .navigationDestination { screenNumber in
      ScreenView(number: screenNumber)
    }
    
    struct ScreenView: View {
      let number: Int 
      var body: some View { Text("This is screen \(number)") }
    }
    

    or just ignore the value and use some other logic.

    .navigationDestination { _ in
       if Calendar.autoupdating.isDateInWeekend(.now) {
         return WeekendView()
       } else {
         return WeekdayView()
       }
    }