12

I'm currently developing an app using NavigationStack. I wonder where should I put the NavigationPath variable so I can modify it anywhere.

I tried to put it inside the root view as a State variable, but it seems difficult for me to modify it inside deeply-nested views.

struct RootView: View {
    @State var path = NavigationPath()
    
    var body: some View {
        NavigationStack(path: $path) {
            // ...
        }
    }
}

struct SubView: View {
    var body: some View {
        Button("Push to root") {
            // how should I access `path` from here?
        }
    }
}

I also tried to put it as a global variable, but this may result that navigation are shared among all scene, which is not what I intended.

class Router: ObservableObject {
    static var shared = Router()
    @Published var path = NavigationPath()
}

struct RootView: View {
    @ObservedObject var router = Router.shared
    
    var body: some View {
        NavigationStack(path: $router.path) {
            // ...
        }
    }
}

struct SubView: View {
    @ObservedObject var router = Router.shared
    
    var body: some View {
        Button("Push to root") {
            router.path = []
        }
    }
}

I would appreciate if someone were to provide a workable design or any suggestions.

wheatset
  • 339
  • 1
  • 7
  • 1
    This might help you https://stackoverflow.com/questions/73723771/navigationstack-not-affected-by-environmentobject-changes/ – Nirav D Dec 15 '22 at 08:43

3 Answers3

1

Use @Binding the same as you would to pass down write access to any other kind of @State. Try to stick with value types in Swift instead of falling back to familiar objects to solve problems, that'll lead to the kind of consistency bugs that Swift/SwiftUI's use of value semantics was designed to eliminate.

malhal
  • 26,330
  • 7
  • 115
  • 133
  • 1
    This turns complicated when the view hierarchy gets deeper as the binding must be passed all along the way. Is there a simpler solution? – wheatset Dec 16 '22 at 16:10
  • 2
    You can put a binding to the path in an `EnvironmentKey` – malhal Dec 16 '22 at 16:19
0

On First time I use NavigationStack and NavigationPath I was thinking as you, depend only on one global path, but I found out I cannot do it this way, because it create a lot of issues

So what I realize is every Main View need one NavigationPath you can use your second solution but add multiple paths

class Router: ObservableObject {
    static var shared = Router()
    @Published var pathHome = NavigationPath()
    @Published var pathSearch = NavigationPath()
}

Now on main view I create StateObject and pass it to TabView using environmentObject

    @EnvironmentObject private var router: Router.shared

    TabView()
    .environmentObject(router)

inside TabView()

TabView {
    HomeView()
     .environmentObject(router)
        .tabItem {
            Label("Home", systemImage: "tray.and.arrow.down.fill")
        }

    SearchView()
     .environmentObject(router)
        .tabItem {
            Label("Search", systemImage: "tray.and.arrow.up.fill")
        }
}

inside HomeView

struct HomeView: View {
    @EnvironmentObject private var router: Router.shared

    var body: some View {
        NavigationStack(path: $router.pathHome) {
           // homeView content...
        }
    }
}

Using this way any views inside HomeView will depend on $router.pathHome if you have HomeDetailsView(subview) you can access to router without need pass it using Binding just by adding

@EnvironmentObject private var router: Router.shared

On SearchView you will depend on router.pathSearch

Basel
  • 550
  • 8
  • 21
0

Use a global router like below.

  1. Define a Router class which conforms to ObservableObject protocol and has a NavigationPath variable.
class Router: ObservableObject {
    @Published var path: NavigationPath = NavigationPath()

    static let shared: Router = Router()
}
  1. Update root view codes.
struct HomePage: View {
    @StateObject var router = Router.shared
    // 1. init router with Router.shared.
    @StateObject var router = Router.shared

    var body: some View {
        // 2. init NavigationStack with $router.path
        NavigationStack(path: $router.path) {
            Button {
                // 4. push new page
                Router.shared.path.append(category)
            } label: {
                Text("Hello world")
            }
            // 3. define navigation destinations
            .navigationDestination(for: Category.self, destination: { value in
                CategoryPage(category: value)
            })
            .navigationDestination(for: Product.self, destination: { value in
                ProductPage(product: value)
            })
    }

  1. Update child view codes.
struct CategoryPage: View {
    var body: some View {
        Button {
            // push new page with Router
            Router.shared.path.append(product)
        } label: {
            Text("Hello world")
        }
}
Steven
  • 99
  • 5