Logo for tanaschita.com

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.

Sponsorship logo
Architecture & Design Patterns for iOS
This book is a practical guide to essential architectural principles and design patterns for iOS development. It covers strategies for building maintainable apps with Swift and SwiftUI, including dependency injection, navigation, common patterns, and modularization.
LEARN MORE

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:

@Observable
class AppRouter {
var searchRouter = SearchRouter()
var settingsRouter = SettingsRouter()
var selectedTab = Tab.search.rawValue
func navigateTo(tab: Tab) {
selectedTab = tab.rawValue
}
}
enum Tab: Int {
case search = 0
case 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:

@Observable
class 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: SearchRouter
var body: some View {
NavigationStack(path: $router.navigationPath) {
SearchView()
.navigationDestination(for: SearchRouter.Route.self) { route in
switch 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 in
if 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.

Sponsorship logo
Preparing for a technical iOS job interview
Preparing for a technical iOS Job Interview with over 300 questions & answers. Covering Swift & Objective-C, SwiftUI & UIKit, Combine, HTTP Networking, iOS File System, Core Data, Concurrency with async/await, Security, Automated Testing, Dependency Management, AI & Machine Learning and more.
LEARN MORE
Sponsorship logo
Architecture & Design Patterns for iOS
This book is a practical guide to essential architectural principles and design patterns for iOS development. It covers strategies for building maintainable apps with Swift and SwiftUI, including dependency injection, navigation, common patterns, and modularization.
LEARN MORE
Sponsorship logo
Become a sponsor of tanaschita.com
By publishing an article on different iOS topics every week, tanaschita.com is constantly growing in the developer community and may provide a great audience for you as a sponsor.
CLICK TO LEARN MORE

Newsletter

Image of a reading marmot
Subscribe

Like to support my work?

Say hi

Related tags

Articles with related topics

navigation

swiftui

swift

ios

How to persist navigation state in SwiftUI

Learn how to save and restore NavigationPath to maintain user context across app launches.

21 Apr 2025 · 4 min read

Latest articles and tips

© 2026 tanaschita.com

Privacy policy

Impressum