Disclosure: This article was created or edited with AI assistance. Revised on 13 October 2026 to remove promotional links and unsupported outcome claims. It is educational guidance, not a verified personal case study or a measured result. Examples are illustrative; use only your own documented experience and measurements.
The sections below present options and illustrative examples. Adapt them to the requirements of your project or role; they do not establish a universal rule or promise a result.
NavigationStack Basics
NavigationStack supports stack-based navigation. Consider NavigationSplitView for a multi-column interface.
struct ContentView: View {
var body: some View {
NavigationStack {
List {
NavigationLink("Profile", value: Route.profile)
NavigationLink("Settings", value: Route.settings)
}
.navigationDestination(for: Route.self) { route in
switch route {
case .profile: ProfileView()
case .settings: SettingsView()
case .detail(let id): Text("Detail: \(id)")
}
}
}
}
}
enum Route: Hashable {
case profile
case settings
case detail(id: String)
}
Value-based links are useful when you need a controllable path. Destination-based links remain valid for simpler flows.
Programmatic Navigation with NavigationPath
The real power comes when you control the navigation stack programmatically.
struct AppView: View {
@State private var path = NavigationPath()
var body: some View {
NavigationStack(path: $path) {
HomeView(path: $path)
.navigationDestination(for: Route.self) { route in
destinationView(for: route)
}
}
}
@ViewBuilder
func destinationView(for route: Route) -> some View {
switch route {
case .profile:
ProfileView(path: $path)
case .settings:
SettingsView()
case .detail(let id):
DetailView(id: id, path: $path)
}
}
}
// Push programmatically from anywhere
struct HomeView: View {
@Binding var path: NavigationPath
var body: some View {
Button("Go to Profile") {
path.append(Route.profile)
}
}
}
Pro tip: You can pop to root by resetting the path:
path = NavigationPath() // Pops to root instantly
The Router Pattern
For larger apps, centralize navigation logic in a Router:
@Observable
class Router {
var path = NavigationPath()
func push(_ route: Route) {
path.append(route)
}
func pop() {
guard !path.isEmpty else { return }
path.removeLast()
}
func popToRoot() {
path = NavigationPath()
}
func reset(to route: Route) {
path = NavigationPath()
path.append(route)
}
}
// Inject via environment
struct AppView: View {
@State private var router = Router()
var body: some View {
NavigationStack(path: $router.path) {
HomeView()
.navigationDestination(for: Route.self) { route in
destinationView(for: route)
}
}
.environment(router)
}
}
// Use from any child view
struct DetailView: View {
@Environment(Router.self) var router
let id: String
var body: some View {
VStack {
Text("Detail: \(id)")
Button("Back to Home") {
router.popToRoot()
}
}
}
}
This pattern gives you clean, testable navigation without the complexity of a full Coordinator pattern.
Tab-Based Navigation
Most real apps combine tabs with navigation stacks:
struct MainTabView: View {
@State private var selectedTab = 0
@State private var homeRouter = Router()
@State private var profileRouter = Router()
var body: some View {
TabView(selection: $selectedTab) {
NavigationStack(path: $homeRouter.path) {
HomeView()
.navigationDestination(for: Route.self) { route in
// home destinations
}
}
.tabItem { Label("Home", systemImage: "house") }
.tag(0)
.environment(homeRouter)
NavigationStack(path: $profileRouter.path) {
ProfileView()
.navigationDestination(for: Route.self) { route in
// profile destinations
}
}
.tabItem { Label("Profile", systemImage: "person") }
.tag(1)
.environment(profileRouter)
}
}
}
Important: Each tab needs its own NavigationStack and Router. Sharing a single path across tabs leads to bugs.
Deep Linking
Handle deep links by parsing URLs into routes and pushing them onto the stack:
struct AppView: View {
@State private var router = Router()
var body: some View {
NavigationStack(path: $router.path) {
HomeView()
.navigationDestination(for: Route.self) { route in
destinationView(for: route)
}
}
.environment(router)
.onOpenURL { url in
handleDeepLink(url)
}
}
func handleDeepLink(_ url: URL) {
guard let components = URLComponents(url: url, resolvingAgainstBaseURL: true),
let host = components.host else { return }
router.popToRoot()
switch host {
case "profile":
router.push(.profile)
case "item":
if let id = components.queryItems?.first(where: { $0.name == "id" })?.value {
router.push(.detail(id: id))
}
default:
break
}
}
}
Sheet and Modal Navigation
Sheets need separate handling. Use an enum for sheet types:
enum SheetType: Identifiable {
case newPost
case editProfile
case settings
var id: String { String(describing: self) }
}
struct HomeView: View {
@State private var activeSheet: SheetType?
var body: some View {
VStack {
Button("New Post") { activeSheet = .newPost }
Button("Edit Profile") { activeSheet = .editProfile }
}
.sheet(item: $activeSheet) { sheet in
switch sheet {
case .newPost: NewPostView()
case .editProfile: EditProfileView()
case .settings: SettingsView()
}
}
}
}
Common Mistakes
- Choosing links without considering state — Use value-based links when you need a controllable path.
- Unintentionally nesting NavigationStacks — Give each independent flow a clearly owned stack.
- Unclear state ownership — Choose and document where navigation state lives.
- Not making Route Hashable — NavigationPath requires Hashable conformance
- Forgetting .navigationDestination — Register all route types at the NavigationStack level
Summary
Modern SwiftUI navigation is powerful when you follow these principles:
- Use NavigationStack with NavigationPath for programmatic control
- Centralize navigation in a Router object
- Separate navigation stacks per tab
- Handle deep links by mapping URLs to routes
- Use sheet(item:) for modal presentation
Scope of the code examples
These are separate teaching snippets, not a complete tested application or a production-ready package. Match availability to your deployment target, provide omitted model/service definitions, and check errors, cancellation, accessibility and persistence before adapting them.
NavigationStack and NavigationSplitView serve different layouts; destination-based links remain valid. See Apple's navigation guide and NavigationSplitView. Deep-link examples also need scheme/host validation and an explicit policy for unavailable or unauthorized destinations.
Top comments (0)