---
name: swiftui-navigation
description: "Implement SwiftUI navigation patterns including NavigationStack, NavigationSplitView, sheet presentation, tab-based navigation, and deep linking. Use when building push navigation, programmatic routing, multi-column layouts, modal sheets, tab bars, universal links, or custom URL scheme handling."
---

# SwiftUI Navigation

Navigation patterns for SwiftUI apps targeting iOS 26+ with Swift 6.2. Covers push navigation, multi-column layouts, sheet presentation, tab architecture, and deep linking. Patterns are backward-compatible to iOS 17 unless noted.

## Contents

- [NavigationStack (Push Navigation)](#navigationstack-push-navigation)
- [NavigationSplitView (Multi-Column)](#navigationsplitview-multi-column)
- [Sheet Presentation](#sheet-presentation)
- [Tab-Based Navigation](#tab-based-navigation)
- [Deep Links](#deep-links)
- [Common Mistakes](#common-mistakes)
- [Review Checklist](#review-checklist)
- [References](#references)

## NavigationStack (Push Navigation)

Use `NavigationStack` with a `NavigationPath` binding for programmatic, type-safe push navigation. Define routes as a `Hashable` enum and map them with `.navigationDestination(for:)`.

```swift
struct ContentView: View {
    @State private var path = NavigationPath()

    var body: some View {
        NavigationStack(path: $path) {
            List(items) { item in
                NavigationLink(value: item) {
                    ItemRow(item: item)
                }
            }
            .navigationDestination(for: Item.self) { item in
                DetailView(item: item)
            }
            .navigationTitle("Items")
        }
    }
}
```

**Programmatic navigation:**

```swift
path.append(item)        // Push
path.removeLast()        // Pop one
path = NavigationPath()  // Pop to root
```

**Router pattern:** For apps with complex navigation, use a router object that owns the path and sheet state. Each tab gets its own router instance injected via `.environment()`. Centralize destination mapping with a single `.navigationDestination(for:)` block or a shared `withAppRouter()` modifier.

See `references/navigationstack.md` for full router examples including per-tab stacks, centralized destination mapping, and generic tab routing.

## NavigationSplitView (Multi-Column)

Use `NavigationSplitView` for sidebar-detail layouts on iPad and Mac. Falls back to stack navigation on iPhone.

```swift
struct MasterDetailView: View {
    @State private var selectedItem: Item?

    var body: some View {
        NavigationSplitView {
            List(items, selection: $selectedItem) { item in
                NavigationLink(value: item) { ItemRow(item: item) }
            }
            .navigationTitle("Items")
        } detail: {
            if let item = selectedItem {
                ItemDetailView(item: item)
            } else {
                ContentUnavailableView("Select an Item", systemImage: "sidebar.leading")
            }
        }
    }
}
```

### Custom Split Column (Manual HStack)

For custom multi-column layouts (e.g., a dedicated notification column independent of selection), use a manual `HStack` split with `horizontalSizeClass` checks:

```swift
@MainActor
struct AppView: View {
  @Environment(\.horizontalSizeClass) private var horizontalSizeClass
  @AppStorage("showSecondaryColumn") private var showSecondaryColumn = true

  var body: some View {
    HStack(spacing: 0) {
      primaryColumn
      if shouldShowSecondaryColumn {
        Divider().edgesIgnoringSafeArea(.all)
        secondaryColumn
      }
    }
  }

  private var shouldShowSecondaryColumn: Bool {
    horizontalSizeClass == .regular
      && showSecondaryColumn
  }

  private var primaryColumn: some View {
    TabView { /* tabs */ }
  }

  private var secondaryColumn: some View {
    NotificationsTab()
      .environment(\.isSecondaryColumn, true)
      .frame(maxWidth: .secondaryColumnWidth)
  }
}
```

Use the manual HStack split when you need full control or a non-standard secondary column. Use `NavigationSplitView` when you want a standard system layout with minimal customization.

## Sheet Presentation

Prefer `.sheet(item:)` over `.sheet(isPresented:)` when state represents a selected model. Sheets should own their actions and call `dismiss()` internally.

```swift
@State private var selectedItem: Item?

.sheet(item: $selectedItem) { item in
    EditItemSheet(item: item)
}
```

**Presentation sizing (iOS 18+):** Control sheet dimensions with `.presentationSizing`:

```swift
.sheet(item: $selectedItem) { item in
    EditItemSheet(item: item)
        .presentationSizing(.form)  // .form, .page, .fitted, .automatic
}
```

`PresentationSizing` values:
- `.automatic` -- platform default
- `.page` -- roughly paper size, for informational content
- `.form` -- slightly narrower than page, for form-style UI
- `.fitted` -- sized by the content's ideal size

Fine-tuning: `.fitted(horizontal:vertical:)` constrains fitting axes; `.sticky(horizontal:vertical:)` grows but does not shrink in specified dimensions.

**Dismissal confirmation (macOS 15+ / iOS 26+):** Use `.dismissalConfirmationDialog("Discard?", shouldPresent: hasUnsavedChanges)` to prevent accidental dismissal of sheets with unsaved changes.

**Enum-driven sheet routing:** Define a `SheetDestination` enum that is `Identifiable`, store it on the router, and map it with a shared view modifier. This lets any child view present sheets without prop-drilling. See `references/sheets.md` for the full centralized sheet routing pattern.

## Tab-Based Navigation

Use the `Tab` API with a selection binding for scalable tab architecture. Each tab should wrap its content in an independent `NavigationStack`.

```swift
struct MainTabView: View {
    @State private var selectedTab: AppTab = .home

    var body: some View {
        TabView(selection: $selectedTab) {
            Tab("Home", systemImage: "house", value: .home) {
                NavigationStack { HomeView() }
            }
            Tab("Search", systemImage: "magnifyingglass", value: .search) {
                NavigationStack { SearchView() }
            }
            Tab("Profile", systemImage: "person", value: .profile) {
                NavigationStack { ProfileView() }
            }
        }
    }
}
```

**Custom binding with side effects:** Route selection changes through a function to intercept special tabs (e.g., compose) that should trigger an action instead of changing selection.

### iOS 26 Tab Additions

- **`Tab(role: .search)`** -- replaces the tab bar with a search field when active
- **`.tabBarMinimizeBehavior(_:)`** -- `.onScrollDown`, `.onScrollUp`, `.never` (iPhone only)
- **`.tabViewSidebarHeader/Footer`** -- customize sidebar sections on iPadOS/macOS
- **`.tabViewBottomAccessory { }`** -- attach content below the tab bar (e.g., Now Playing bar)
- **`TabSection`** -- group tabs into sidebar sections with `.tabPlacement(.sidebarOnly)`

See `references/tabview.md` for full TabView patterns including custom bindings, dynamic tabs, and sidebar customization.

## Deep Links

### Universal Links

Universal links let iOS open your app for standard HTTPS URLs. They require:
1. An Apple App Site Association (AASA) file at `/.well-known/apple-app-site-association`
2. An Associated Domains entitlement (`applinks:example.com`)

Handle in SwiftUI with `.onOpenURL` and `.onContinueUserActivity`:

```swift
@main
struct MyApp: App {
    @State private var router = Router()

    var body: some Scene {
        WindowGroup {
            ContentView()
                .environment(router)
                .onOpenURL { url in router.handle(url: url) }
                .onContinueUserActivity(NSUserActivityTypeBrowsingWeb) { activity in
                    guard let url = activity.webpageURL else { return }
                    router.handle(url: url)
                }
        }
    }
}
```

### Custom URL Schemes

Register schemes in `Info.plist` under `CFBundleURLTypes`. Handle with `.onOpenURL`. Prefer universal links over custom schemes for publicly shared links -- they provide web fallback and domain verification.

### Handoff (NSUserActivity)

Advertise activities with `.userActivity()` and receive them with `.onContinueUserActivity()`. Declare activity types in `Info.plist` under `NSUserActivityTypes`. Set `isEligibleForHandoff = true` and provide a `webpageURL` as fallback.

See `references/deeplinks.md` for full examples of AASA configuration, router URL handling, custom URL schemes, and NSUserActivity continuation.

## Common Mistakes

1. Using deprecated `NavigationView` -- use `NavigationStack` or `NavigationSplitView`
2. Sharing one `NavigationPath` across all tabs -- each tab needs its own path
3. Using `.sheet(isPresented:)` when state represents a model -- use `.sheet(item:)` instead
4. Storing view instances in `NavigationPath` -- store lightweight `Hashable` route data
5. Nesting `@Observable` router objects inside other `@Observable` objects
6. Using deprecated `.tabItem { }` API -- use `Tab(value:)` with `TabView(selection:)`
7. Assuming `tabBarMinimizeBehavior` works on iPad -- it is iPhone only
8. Handling deep links in multiple places -- centralize URL parsing in the router
9. Hard-coding sheet frame dimensions -- use `.presentationSizing(.form)` instead
10. Missing `@MainActor` on router classes -- required for Swift 6 concurrency safety

## Review Checklist

- [ ] `NavigationStack` used (not `NavigationView`)
- [ ] Each tab has its own `NavigationStack` with independent path
- [ ] Route enum is `Hashable` with stable identifiers
- [ ] `.navigationDestination(for:)` maps all route types
- [ ] `.sheet(item:)` preferred over `.sheet(isPresented:)`
- [ ] Sheets own their dismiss logic internally
- [ ] Router object is `@MainActor` and `@Observable`
- [ ] Deep link URLs parsed and validated before navigation
- [ ] Universal links have AASA and Associated Domains configured
- [ ] Tab selection uses `Tab(value:)` with binding

## References

- NavigationStack and router patterns: `references/navigationstack.md`
- Sheet presentation and routing: `references/sheets.md`
- TabView patterns and iOS 26 API: `references/tabview.md`
- Deep links, universal links, and Handoff: `references/deeplinks.md`
- Architecture and state management: see `swiftui-patterns` skill
- Layout and components: see `swiftui-layout-components` skill
