Using NavigationPath with TabView in SwiftUI
Learn how to integrate NavigationPath with TabView in SwiftUI to manage independent navigation flows.
14 Apr 2025 · 5 min read
When building complex iOS applications, we often need to combine tab-based navigation with stack-based navigation. In SwiftUI, this means integrating TabView with NavigationStack. A common challenge in this scenario is managing navigation state properly across multiple tabs while maintaining independent navigation flows.
Let's look at one possible solution. We'll create a central app router to coordinate navigation and give each tab its own router and NavigationPath. This way, we will gain full control of the navigation, allowing for clean separation of concerns, deep linking support, and a more modular design.

Designing the app router
We'll start by creating an app-wide router, which allows us to switch between tabs and go to any place in our iOS application.
Here is an example implementation:
@Observableclass AppRouter {var searchRouter = SearchRouter()var settingsRouter = SettingsRouter()var selectedTab = Tab.search.rawValuefunc navigateTo(tab: Tab) {selectedTab = tab.rawValue}}enum Tab: Int {case search = 0case settings = 1}
The AppRouter is responsible for holding the currently selected tab and routers for each tab's navigation flow.
Creating per-tab routers
Since each tab needs it's own navigation stack, we create a separate router for each tab. A tab router owns a NavigationPath and defines the types of views (routes) that can be pushed onto the navigation stack.
For example:
@Observableclass SearchRouter {var navigationPath = NavigationPath()enum Route: Hashable {case detail(itemId: String)}func navigateTo(route: Route) {navigationPath.append(route)}}
In the example above, we defined the router for a search flow, where a user can navigate to a detail view after searching for an item.
Putting it together with TabView and NavigationStack
With the AppRouter and individual tab routers in place, we can wire everything together inside our root view:
struct RootTabView: View {@State private var appRouter = AppRouter()var body: some View {TabView(selection: $appRouter.selectedTab) {SearchRootView(router: appRouter.searchRouter).tabItem { Label("Search", systemImage: "magnifyingglass") }.tag(Tab.search.rawValue)SettingsRootView(router: appRouter.settingsRouter).tabItem { Label("Settings", systemImage: "gearshape") }.tag(Tab.settings.rawValue)}}}
Each tab's root view receives its associated router.
Navigating within a tab
Here's what the root view for the search tab could look like:
struct SearchRootView: View {@Bindable var router: SearchRoutervar body: some View {NavigationStack(path: $router.navigationPath) {SearchView().navigationDestination(for: SearchRouter.Route.self) { route inswitch route {case .detail(let itemId):DetailView(itemId: itemId)}}}}}
By binding NavigationStack to the router.navigationPath, we give the search tab its own independent navigation history.
Navigating to specific views
With the setup shown above, we have now full control of the navigation in our app. For example, we can use it to handle universal links as follows:
.onOpenURL { url inif let universalLink = UniversalLink(from: url) {switch universalLink {case .detail(let itemId):appRouter.navigateTo(tab: Tab.search)appRouter.searchRouter.navigateTo(route: .detail(itemId: itemId))// Handle other cases.}}}
This logic switches to the correct tab and then updates that tab's navigation path to push the appropriate view.
Wrapping up
Using NavigationPath with TabView in SwiftUI is a powerful way to manage complex navigation scenarios. By assigning each tab its own NavigationPath, we avoid the pitfalls of shared navigation state and gain the ability to support deep linking, preserve user context, and scale navigation logic cleanly.



Newsletter
Like to support my work?
Say hi
Related tags
Articles with related topics
Latest articles and tips



