Where to place global NavigationPath in SwiftUI
Asked Answered
B

4

18

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.

Baltic answered 15/12, 2022 at 8:31 Comment(1)
This might help you #73724271Bobcat
I
2

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.

Induce answered 15/12, 2022 at 14:27 Comment(3)
This turns complicated when the view hierarchy gets deeper as the binding must be passed all along the way. Is there a simpler solution?Baltic
You can put a binding to the path in an EnvironmentKeyInduce
Can you please include in your answer a working example? Your arguments are valid, but I can't seem to be able to use mutating methods like navigationPath.appendValueless
C
4

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")
        }
}
Calle answered 22/8, 2023 at 2:39 Comment(0)
B
3

You can use an Environment to pass some action downward:

final class Navigation: ObservableObject {
    @Published var path = NavigationPath()
}

extension EnvironmentValues {
    private struct NavigationKey: EnvironmentKey {
        static let defaultValue = Navigation()
    }

    var navigation: Navigation {
        get { self[NavigationKey.self] }
        set { self[NavigationKey.self] = newValue }
    }
}

Your rootview then:

struct RootView: View {
    @StateObject private var navigation = Navigation()

    var body: some View {
        NavigationStack(path: $navigation.path) {
        // ...
        }
        .environment(\.navigation, navigation)
    }
}

Your subview:

struct SubView: View {
    @Environment(\.navigation) private var navigation

    var body: some View {
        Button("Push to root") {
            navigation.path = NavigationPath() // i.e pop to root
        }
    }
}
Benefield answered 17/5, 2024 at 11:3 Comment(0)
I
2

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.

Induce answered 15/12, 2022 at 14:27 Comment(3)
This turns complicated when the view hierarchy gets deeper as the binding must be passed all along the way. Is there a simpler solution?Baltic
You can put a binding to the path in an EnvironmentKeyInduce
Can you please include in your answer a working example? Your arguments are valid, but I can't seem to be able to use mutating methods like navigationPath.appendValueless
I
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

Importunity answered 15/5, 2023 at 10:8 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.