axiom-swiftui-nav-ref

Reference — Comprehensive SwiftUI navigation guide covering NavigationStack (iOS 16+), NavigationSplitView (iOS 16+), NavigationPath, deep linking, state restoration, Tab+Navigation integration (iOS 18+), Liquid Glass navigation (iOS 26+), and coordinator patterns

25 stars

Best use case

axiom-swiftui-nav-ref is best used when you need a repeatable AI agent workflow instead of a one-off prompt.

Reference — Comprehensive SwiftUI navigation guide covering NavigationStack (iOS 16+), NavigationSplitView (iOS 16+), NavigationPath, deep linking, state restoration, Tab+Navigation integration (iOS 18+), Liquid Glass navigation (iOS 26+), and coordinator patterns

Teams using axiom-swiftui-nav-ref should expect a more consistent output, faster repeated execution, less prompt rewriting.

When to use this skill

  • You want a reusable workflow that can be run more than once with consistent structure.

When not to use this skill

  • You only need a quick one-off answer and do not need a reusable workflow.
  • You cannot install or maintain the underlying files, dependencies, or repository context.

Installation

Claude Code / Cursor / Codex

$curl -o ~/.claude/skills/axiom-swiftui-nav-ref/SKILL.md --create-dirs "https://raw.githubusercontent.com/ComeOnOliver/skillshub/main/skills/CharlesWiltgen/Axiom/axiom-swiftui-nav-ref/SKILL.md"

Manual Installation

  1. Download SKILL.md from GitHub
  2. Place it in .claude/skills/axiom-swiftui-nav-ref/SKILL.md inside your project
  3. Restart your AI agent — it will auto-discover the skill

How axiom-swiftui-nav-ref Compares

Feature / Agentaxiom-swiftui-nav-refStandard Approach
Platform SupportNot specifiedLimited / Varies
Context Awareness High Baseline
Installation ComplexityUnknownN/A

Frequently Asked Questions

What does this skill do?

Reference — Comprehensive SwiftUI navigation guide covering NavigationStack (iOS 16+), NavigationSplitView (iOS 16+), NavigationPath, deep linking, state restoration, Tab+Navigation integration (iOS 18+), Liquid Glass navigation (iOS 26+), and coordinator patterns

Where can I find the source code?

You can find the source code on GitHub using the link provided at the top of the page.

SKILL.md Source

# SwiftUI Navigation API Reference

## Overview

SwiftUI's navigation APIs provide data-driven, programmatic navigation that scales from simple stacks to complex multi-column layouts. Introduced in iOS 16 (2022) with NavigationStack and NavigationSplitView, evolved in iOS 18 (2024) with Tab/Sidebar unification, and refined in iOS 26 (2025) with Liquid Glass design.

#### Evolution timeline
- **2022 (iOS 16)** NavigationStack, NavigationSplitView, NavigationPath, value-based NavigationLink
- **2024 (iOS 18)** Tab/Sidebar unification, sidebarAdaptable style, zoom navigation transition
- **2025 (iOS 26)** Liquid Glass navigation chrome, bottom-aligned search, floating tab bars, backgroundExtensionEffect

#### Key capabilities
- **Data-driven navigation** NavigationPath represents stack state, enabling programmatic push/pop and deep linking
- **Multi-column layouts** NavigationSplitView adapts automatically (3-column on iPad → single stack on iPhone)
- **State restoration** Codable NavigationPath + SceneStorage for persistence across app launches
- **Tab integration** Per-tab NavigationStack with state preservation on tab switch (iOS 18+)
- **Liquid Glass** Automatic glass navigation bars, sidebars, and toolbars (iOS 26+)

#### When to use vs UIKit
- **SwiftUI navigation** New apps, multiplatform, simpler navigation flows → Use NavigationStack/SplitView
- **UINavigationController** Complex coordinator patterns, legacy code, specific UIKit features → Consider UIKit

#### Related Skills
- Use `axiom-swiftui-nav` for anti-patterns, decision trees, pressure scenarios
- Use `axiom-swiftui-nav-diag` for systematic troubleshooting of navigation issues

---

## When to Use This Skill

Use this skill when:
- **Implementing navigation APIs** NavigationStack, NavigationSplitView, NavigationPath, Tab+Navigation
- **Deep linking or state restoration** URL routing, Codable NavigationPath, SceneStorage
- **Adopting iOS 26+ features** Liquid Glass navigation, bottom-aligned search, tab bar minimization
- **Choosing navigation architecture** Stack vs SplitView vs coordinator patterns

---

## API Evolution

### Timeline

| Year | iOS Version | Key Features |
|------|-------------|--------------|
| 2020 | iOS 14 | NavigationView (deprecated iOS 16) |
| 2022 | iOS 16 | NavigationStack, NavigationSplitView, NavigationPath, value-based NavigationLink |
| 2024 | iOS 18 | Tab/Sidebar unification, sidebarAdaptable, TabSection, zoom transitions |
| 2025 | iOS 26 | Liquid Glass navigation, backgroundExtensionEffect, tabBarMinimizeBehavior |

### NavigationView (Deprecated)

NavigationView is deprecated as of iOS 16. Use NavigationStack (single-column push/pop) or NavigationSplitView (multi-column) exclusively in new code. Key improvements: single NavigationPath replaces per-link `isActive` bindings, value-based type safety, built-in Codable state restoration. See "Migrating to new navigation types" documentation.

---

## NavigationStack Complete Reference

NavigationStack represents a push-pop interface like Settings on iPhone or System Settings on macOS.

### 1.1 Creating NavigationStack

#### Basic NavigationStack

```swift
NavigationStack {
    List(Category.allCases) { category in
        NavigationLink(category.name, value: category)
    }
    .navigationTitle("Categories")
    .navigationDestination(for: Category.self) { category in
        CategoryDetail(category: category)
    }
}
```

#### With Path Binding (WWDC 2022, 6:05)

```swift
struct PushableStack: View {
    @State private var path: [Recipe] = []
    @StateObject private var dataModel = DataModel()

    var body: some View {
        NavigationStack(path: $path) {
            List(Category.allCases) { category in
                Section(category.localizedName) {
                    ForEach(dataModel.recipes(in: category)) { recipe in
                        NavigationLink(recipe.name, value: recipe)
                    }
                }
            }
            .navigationTitle("Categories")
            .navigationDestination(for: Recipe.self) { recipe in
                RecipeDetail(recipe: recipe)
            }
        }
        .environmentObject(dataModel)
    }
}
```

Path binding + value-presenting NavigationLink + `navigationDestination(for:)` form the core data-driven navigation pattern.

### 1.2 NavigationLink (Value-Based)

#### Value-presenting NavigationLink

```swift
// Correct: Value-based (iOS 16+)
NavigationLink(recipe.name, value: recipe)

// Correct: With custom label
NavigationLink(value: recipe) {
    RecipeTile(recipe: recipe)
}

// Deprecated: View-based (iOS 13-15)
NavigationLink(recipe.name) {
    RecipeDetail(recipe: recipe)  // Don't use in new code
}
```

#### How NavigationLink works with NavigationStack

1. NavigationStack maintains a `path` collection
2. Tapping a value-presenting link appends the value to the path
3. NavigationStack maps `navigationDestination` modifiers over path values
4. Views are pushed onto the stack based on destination mappings

### 1.3 navigationDestination Modifier

#### Single Type

```swift
.navigationDestination(for: Recipe.self) { recipe in
    RecipeDetail(recipe: recipe)
}
```

#### Multiple Types

```swift
NavigationStack(path: $path) {
    RootView()
        .navigationDestination(for: Recipe.self) { recipe in
            RecipeDetail(recipe: recipe)
        }
        .navigationDestination(for: Category.self) { category in
            CategoryList(category: category)
        }
        .navigationDestination(for: Chef.self) { chef in
            ChefProfile(chef: chef)
        }
}
```

#### Navigation Anti-Patterns

- **Never mix `navigationDestination(for:)` and `NavigationLink(destination:)`** in the same NavigationStack hierarchy — causes undefined behavior
- **Register `navigationDestination(for:)` once per data type** — duplicates cause the wrong view to appear

#### Placement rules
- Place `navigationDestination` outside lazy containers (not inside ForEach)
- Place near related NavigationLinks for code organization
- Must be inside NavigationStack hierarchy

```swift
// Correct: Outside lazy container
ScrollView {
    LazyVGrid(columns: columns) {
        ForEach(recipes) { recipe in
            NavigationLink(value: recipe) {
                RecipeTile(recipe: recipe)
            }
        }
    }
}
.navigationDestination(for: Recipe.self) { recipe in
    RecipeDetail(recipe: recipe)
}

// Wrong: Inside ForEach (may not be loaded)
ForEach(recipes) { recipe in
    NavigationLink(value: recipe) { RecipeTile(recipe: recipe) }
        .navigationDestination(for: Recipe.self) { r in  // Don't do this
            RecipeDetail(recipe: r)
        }
}
```

### 1.4 NavigationPath

NavigationPath is a type-erased collection for heterogeneous navigation stacks.

#### Typed Array vs NavigationPath

```swift
// Typed array: All values same type
@State private var path: [Recipe] = []

// NavigationPath: Mixed types
@State private var path = NavigationPath()
```

#### NavigationPath Operations

```swift
// Append value
path.append(recipe)

// Pop to previous
path.removeLast()

// Pop to root
path.removeLast(path.count)
// or
path = NavigationPath()

// Check count
if path.count > 0 { ... }

// Deep link: Set multiple values
path.append(category)
path.append(recipe)
```

#### Codable Support

```swift
// NavigationPath is Codable when all values are Codable
@State private var path = NavigationPath()

// Encode
let data = try JSONEncoder().encode(path.codable)

// Decode
let codableRep = try JSONDecoder().decode(NavigationPath.CodableRepresentation.self, from: data)
path = NavigationPath(codableRep)
```

---

## NavigationSplitView Complete Reference

NavigationSplitView creates multi-column layouts that adapt to device size.

### 2.1 Two-Column Layout

#### Basic Two-Column (WWDC 2022, 10:40)

```swift
struct MultipleColumns: View {
    @State private var selectedCategory: Category?
    @State private var selectedRecipe: Recipe?
    @StateObject private var dataModel = DataModel()

    var body: some View {
        NavigationSplitView {
            List(Category.allCases, selection: $selectedCategory) { category in
                NavigationLink(category.localizedName, value: category)
            }
            .navigationTitle("Categories")
        } detail: {
            if let recipe = selectedRecipe {
                RecipeDetail(recipe: recipe)
            } else {
                Text("Select a recipe")
            }
        }
    }
}
```

### 2.2 Three-Column Layout

Add a `content:` closure between sidebar and detail for a middle column:

```swift
NavigationSplitView {
    List(Category.allCases, selection: $selectedCategory) { category in
        NavigationLink(category.localizedName, value: category)
    }
} content: {
    List(dataModel.recipes(in: selectedCategory), selection: $selectedRecipe) { recipe in
        NavigationLink(recipe.name, value: recipe)
    }
} detail: {
    RecipeDetail(recipe: selectedRecipe)
}
```

### 2.3 NavigationSplitView with NavigationStack

Place a `NavigationStack(path:)` inside the detail column for grid-to-detail drill-down while preserving sidebar selection:

```swift
NavigationSplitView {
    List(Category.allCases, selection: $selectedCategory) { ... }
} detail: {
    NavigationStack(path: $path) {
        RecipeGrid(category: selectedCategory)
            .navigationDestination(for: Recipe.self) { recipe in
                RecipeDetail(recipe: recipe)
            }
    }
}
```

### 2.4 Column Visibility

```swift
@State private var columnVisibility: NavigationSplitViewVisibility = .all

NavigationSplitView(columnVisibility: $columnVisibility) {
    Sidebar()
} content: {
    Content()
} detail: {
    Detail()
}

// Programmatically control visibility
columnVisibility = .detailOnly  // Hide sidebar and content
columnVisibility = .all          // Show all columns
columnVisibility = .automatic    // System decides
```

### 2.5 Automatic Adaptation

NavigationSplitView automatically adapts:
- **iPad landscape** All columns visible (depending on configuration)
- **iPad portrait/Slide Over** Collapses to overlay or single column
- **iPhone** Single navigation stack
- **Apple Watch/TV** Single navigation stack

Selection changes automatically translate to push/pop on iPhone.

### 2.6 iOS 26+ Liquid Glass Sidebar (WWDC 2025, 323)

```swift
NavigationSplitView {
    List { ... }
} detail: {
    DetailView()
}
// Sidebar automatically gets Liquid Glass appearance on iPad/macOS

// Extend content behind glass sidebar
.backgroundExtensionEffect()  // Mirrors and blurs content outside safe area
```

---

## Deep Linking and URL Routing

### 3.1 Deep Link Pattern

Use `.onOpenURL` to receive URLs, parse with `URLComponents`, then manipulate `NavigationPath`:

```swift
.onOpenURL { url in
    guard let components = URLComponents(url: url, resolvingAgainstBaseURL: false),
          let host = components.host else { return }
    path.removeLast(path.count)  // Pop to root first
    // Parse host/path to determine destination, then path.append(value)
}
```

For multi-step deep links (`myapp://category/desserts/recipe/apple-pie`), iterate URL path components and append each resolved value to build the full navigation stack.

For comprehensive deep linking examples, error diagnosis, and testing workflows, see `axiom-swiftui-nav-diag` (Pattern 3).

---

## State Restoration

### 4.1 Complete State Restoration (WWDC 2022, 18:12)

```swift
struct UseSceneStorage: View {
    @StateObject private var navModel = NavigationModel()
    @SceneStorage("navigation") private var data: Data?
    @StateObject private var dataModel = DataModel()

    var body: some View {
        NavigationSplitView {
            List(Category.allCases, selection: $navModel.selectedCategory) { category in
                NavigationLink(category.localizedName, value: category)
            }
            .navigationTitle("Categories")
        } detail: {
            NavigationStack(path: $navModel.recipePath) {
                RecipeGrid(category: navModel.selectedCategory)
            }
        }
        .task {
            // Restore on appear
            if let data = data {
                navModel.jsonData = data
            }
            // Save on changes
            for await _ in navModel.objectWillChangeSequence {
                data = navModel.jsonData
            }
        }
        .environmentObject(dataModel)
    }
}
```

### 4.2 Codable NavigationModel

```swift
class NavigationModel: ObservableObject, Codable {
    @Published var selectedCategory: Category?
    @Published var recipePath: [Recipe] = []

    enum CodingKeys: String, CodingKey {
        case selectedCategory
        case recipePathIds  // Store IDs, not full objects
    }

    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encodeIfPresent(selectedCategory, forKey: .selectedCategory)
        try container.encode(recipePath.map(\.id), forKey: .recipePathIds)
    }

    init() {}

    required init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        self.selectedCategory = try container.decodeIfPresent(Category.self, forKey: .selectedCategory)
        let recipePathIds = try container.decode([Recipe.ID].self, forKey: .recipePathIds)
        self.recipePath = recipePathIds.compactMap { DataModel.shared[$0] } // Discard deleted items
    }
}
```

Store IDs (not full model objects) and use `compactMap` to handle deleted items gracefully. Add `jsonData` computed property and `objectWillChangeSequence` for SceneStorage integration as shown in 4.1.

---

## Tab + Navigation Integration

### 5.1 Tab Syntax (iOS 18+) (WWDC 2024, 4:27)

```swift
TabView {
    Tab("Watch Now", systemImage: "play") {
        WatchNowView()
    }
    Tab("Library", systemImage: "books.vertical") {
        LibraryView()
    }
    Tab(role: .search) {
        NavigationStack {
            SearchView()
                .navigationTitle("Search")
        }
        .searchable(text: $searchText)
    }
}
```

**Search tab requirement**: Contents of a search-role tab must be wrapped in `NavigationStack` with `.searchable()` applied to the stack. Without `NavigationStack`, the search field will not appear. For foundational `.searchable` patterns (suggestions, scopes, tokens, programmatic control), see `axiom-swiftui-search-ref`.

### 5.2 TabView with NavigationStack Per Tab

```swift
TabView {
    Tab("Home", systemImage: "house") {
        NavigationStack {
            HomeView()
                .navigationDestination(for: Item.self) { item in
                    ItemDetail(item: item)
                }
        }
    }
    Tab("Settings", systemImage: "gear") {
        NavigationStack {
            SettingsView()
        }
    }
}
```

Each tab has its own NavigationStack to preserve navigation state when switching tabs.

### 5.3 Sidebar-Adaptable TabView (WWDC 2024, 6:41)

```swift
TabView {
    Tab("Watch Now", systemImage: "play") {
        WatchNowView()
    }
    Tab("Library", systemImage: "books.vertical") {
        LibraryView()
    }
    TabSection("Collections") {
        Tab("Cinematic Shots", systemImage: "list.and.film") {
            CinematicShotsView()
        }
        Tab("Forest Life", systemImage: "list.and.film") {
            ForestLifeView()
        }
    }
    TabSection("Animations") {
        // More tabs...
    }
    Tab(role: .search) {
        SearchView()
    }
}
.tabViewStyle(.sidebarAdaptable)
```

`TabSection` creates sidebar groups. `.sidebarAdaptable` enables sidebar on iPad, tab bar on iPhone. Search tab with `.search` role gets special placement.

### 5.4 Tab Customization (WWDC 2024, 10:45)

```swift
@AppStorage("MyTabViewCustomization")
private var customization: TabViewCustomization

TabView {
    Tab("Watch Now", systemImage: "play", value: .watchNow) {
        WatchNowView()
    }
    .customizationID("Tab.watchNow")
    .customizationBehavior(.disabled, for: .sidebar, .tabBar)  // Can't be hidden

    Tab("Optional Tab", systemImage: "star", value: .optional) {
        OptionalView()
    }
    .customizationID("Tab.optional")
    .defaultVisibility(.hidden, for: .tabBar)  // Hidden by default
}
.tabViewCustomization($customization)
```

### 5.5 Tab Context Menus

Use `.contextMenu(menuItems:)` on a `Tab` to add a context menu to its sidebar representation (e.g., right-click on Mac, long-press on iPad sidebar).

```swift
Tab("Currently Reading", systemImage: "book") {
    CurrentBooksList()
}
.contextMenu {
    Button {
        pinnedTabs.insert(.reading)
    } label: {
        Label("Pin", systemImage: "pin")
    }
    Button {
        showShareSheet = true
    } label: {
        Label("Share", systemImage: "square.and.arrow.up")
    }
}
```

Return an empty closure to deactivate the context menu conditionally:

```swift
.contextMenu {
    if canPin {
        Button("Pin", systemImage: "pin") { pin() }
    }
}
```

#### iPhone Tab Bar Long-Press

`.contextMenu` on `Tab` only applies to the sidebar representation (iPad/Mac). iPhone tab bar context menus require UIKit interop (adding `UILongPressGestureRecognizer` to `UITabBar` via Introspect or a `UITabBarController` subclass). See `axiom-swiftui-nav-diag` for workaround patterns.

**Caveat**: Relies on private `UITabBarButton` subviews — fragile across iOS versions, not a public API guarantee.

### 5.6 Programmatic Tab Visibility

Use `.hidden(_:)` to show/hide tabs based on app state while preserving their navigation state.

#### State-Driven Tab Visibility

```swift
Tab("Libraries", systemImage: "square.stack") { LibrariesView() }
    .hidden(context == .browse)  // Hide based on app state
```

Apply `.hidden(condition)` to each tab. Tabs hidden this way preserve their navigation state (unlike conditional `if` rendering which destroys and recreates them).

#### State Preservation

**Key difference**: `.hidden(_:)` preserves tab state, conditional rendering does not.

```swift
// ✅ State preserved when hidden
Tab("Settings", systemImage: "gear") {
    SettingsView()  // Navigation stack preserved
}
.hidden(!showSettings)

// ❌ State lost when condition changes
if showSettings {
    Tab("Settings", systemImage: "gear") {
        SettingsView()  // Navigation stack recreated
    }
}
```

#### Common Patterns

```swift
Tab("Beta Features", systemImage: "flask") { BetaView() }
    .hidden(!UserDefaults.standard.bool(forKey: "enableBetaFeatures"))
```

Same pattern applies to authentication state, purchase status, and debug builds — bind `.hidden()` to any boolean condition.

#### Animated Transitions

Wrap state changes in `withAnimation` for smooth tab bar layout transitions:

```swift
Button("Switch to Browse") {
    withAnimation {
        context = .browse
        selection = .tracks  // Switch to first visible tab
    }
}
// Tab bar animates as tabs appear/disappear
// Uses system motion curves automatically
```

### 5.7 iOS 26+ Tab Features (WWDC 2025, 256)

```swift
// Tab bar minimization on scroll
TabView { ... }
    .tabBarMinimizeBehavior(.onScrollDown)

// Bottom accessory view (always visible)
TabView { ... }
    .tabViewBottomAccessory {
        PlaybackControls()
    }

// Dynamic visibility (recommended for mini-players)
// ⚠️ Requires iOS 26.1+ (not 26.0)
TabView { ... }
    .tabViewBottomAccessory(isEnabled: showMiniPlayer) {
        MiniPlayerView()
            .transition(.opacity)
    }
// isEnabled: true = shows accessory
// isEnabled: false = hides AND removes reserved space

// Search tab with dedicated search field
Tab(role: .search) {
    NavigationStack {
        SearchView()
            .navigationTitle("Search")
    }
    .searchable(text: $searchText)
}
// Morphs into search field when selected
// ⚠️ NavigationStack wrapper required for search field to appear
// Fallback: If no tab has .search role, the tab view applies search
// to ALL tabs, resetting search state when the selected tab changes
```

#### Dynamic Bottom Accessory

The accessory can switch on `activeTab` for per-tab content, though Apple's usage (Music mini-player) keeps it global. Read `@Environment(\.tabViewBottomAccessoryPlacement)` to adapt layout: `.bar` when above tab bar (full controls), other values when inline with collapsed tab bar (compact).

Reserve `tabViewBottomAccessory` for cross-tab content (playback, status). For tab-specific actions, prefer floating glass buttons within the tab's content view.

### 5.8 Tab API Quick Reference

| Modifier | Target | iOS | Purpose |
|----------|--------|-----|---------|
| `Tab(_:systemImage:value:content:)` | — | 18+ | New tab syntax with selection value |
| `Tab(role: .search)` | — | 18+ | Semantic search tab with morph behavior |
| `TabSection(_:content:)` | — | 18+ | Group tabs in sidebar view |
| `.contextMenu(menuItems:)` | Tab | 18+ | Add context menu to tab's sidebar representation |
| `.customizationID(_:)` | Tab | 18+ | Enable user customization |
| `.customizationBehavior(_:for:)` | Tab | 18+ | Control hide/reorder permissions |
| `.defaultVisibility(_:for:)` | Tab | 18+ | Set initial visibility state |
| `.hidden(_:)` | Tab | 18+ | Programmatic visibility with state preservation |
| `.tabViewStyle(.sidebarAdaptable)` | TabView | 18+ | Sidebar on iPad, tabs on iPhone |
| `.tabViewCustomization($binding)` | TabView | 18+ | Persist user tab arrangement |
| `.tabBarMinimizeBehavior(_:)` | TabView | 26+ | Auto-hide on scroll |
| `.tabViewBottomAccessory(isEnabled:content:)` | TabView | 26.1+ | Dynamic content below tab bar |

---

## iOS 26+ Navigation Features

### 6.1 Liquid Glass Navigation (WWDC 2025, 323)

Automatic adoption when building with Xcode 26:
- Navigation bars become Liquid Glass
- Sidebars float above content with glass effect
- Tab bars float with new compact appearance
- Toolbars get automatic grouping

### 6.2 Background Extension Effect

```swift
NavigationSplitView {
    Sidebar()
} detail: {
    HeroImage()
        .backgroundExtensionEffect()  // Content extends behind sidebar
}
```

### 6.3 Bottom-Aligned Search (WWDC 2025, 256)

**Foundational search APIs** For `.searchable`, `isSearching`, suggestions, scopes, tokens, and programmatic control, see `axiom-swiftui-search-ref`. This section covers iOS 26 bottom-aligned refinement only.

```swift
NavigationSplitView {
    Sidebar()
} detail: {
    DetailView()
}
.searchable(text: $query, prompt: "What are you looking for?")
// Automatically bottom-aligned on iPhone, top-trailing on iPad
```

### 6.4 Scroll Edge Effect

```swift
// Automatic blur effect when content scrolls under toolbar
// Remove any custom darkening backgrounds - they interfere

// For dense UIs, adjust sharpness
ScrollView { ... }
    .scrollEdgeEffectStyle(.soft)  // .sharp, .soft
```

### 6.5 Sheet Presentations with Zoom Transition

In iOS 26, sheets can morph directly out of the buttons that present them. Make the presenting toolbar item a source for a navigation zoom transition, and mark the sheet content as the destination:

```swift
@Namespace private var namespace

// Sheet morphs out of presenting button
.toolbar {
    ToolbarItem {
        Button("Settings") { showSettings = true }
            .matchedTransitionSource(id: "settings", in: namespace)
    }
}
.sheet(isPresented: $showSettings) {
    SettingsView()
        .navigationTransition(.zoom(sourceID: "settings", in: namespace))
}
```

Other presentations also flow smoothly out of Liquid Glass controls — menus, alerts, and popovers. Dialogs automatically morph out of the buttons that present them without additional code.

**Audit tip**: If you've used `presentationBackground` to apply custom backgrounds to sheets, consider removing it and let the new Liquid Glass sheet material shine. Partial height sheets are now inset with glass background by default.

### 6.6 Toolbar Morphing Transitions

iOS 26 automatically morphs toolbars during NavigationStack push/pop when each destination view declares its own `.toolbar {}`. Items with matching `toolbar(id:)` and `ToolbarItem(id:)` IDs stay stable during the transition (no bounce), while unmatched items animate in/out.

**Key rule**: Attach `.toolbar {}` to individual views inside NavigationStack, not to NavigationStack itself. Otherwise there is nothing to morph between.

See `axiom-swiftui-26-ref` skill for complete toolbar morphing API including DefaultToolbarItem, `toolbar(id:)` stable items, ToolbarSpacer patterns, and troubleshooting.

---

## Router/Coordinator Patterns

### 7.1 When to Use Coordinators

**Use coordinators when:**
- Navigation logic is complex with conditional flows
- Testing navigation in isolation
- Sharing navigation logic across multiple screens
- UIKit interop with heavy navigation requirements

**Use built-in navigation when:**
- Simple linear or hierarchical navigation
- State restoration is primary concern
- Fewer than 5-10 navigation destinations
- No need for navigation unit testing

### 7.2 Simple Router Pattern

```swift
// Route enum defines all possible destinations
enum AppRoute: Hashable {
    case home
    case category(Category)
    case recipe(Recipe)
    case settings
}

// Router class manages navigation
@Observable
class Router {
    var path = NavigationPath()

    func navigate(to route: AppRoute) {
        path.append(route)
    }

    func popToRoot() {
        path.removeLast(path.count)
    }

    func pop() {
        if !path.isEmpty {
            path.removeLast()
        }
    }
}

// Usage in views
struct ContentView: View {
    @State private var router = Router()

    var body: some View {
        NavigationStack(path: $router.path) {
            HomeView()
                .navigationDestination(for: AppRoute.self) { route in
                    switch route {
                    case .home:
                        HomeView()
                    case .category(let category):
                        CategoryView(category: category)
                    case .recipe(let recipe):
                        RecipeDetail(recipe: recipe)
                    case .settings:
                        SettingsView()
                    }
                }
        }
        .environment(router)
    }
}

// In child views
struct RecipeCard: View {
    let recipe: Recipe
    @Environment(Router.self) private var router

    var body: some View {
        Button(recipe.name) {
            router.navigate(to: .recipe(recipe))
        }
    }
}
```

### 7.3 Coordinator Pattern with Protocol

For larger apps, extract a `Coordinator` protocol with `associatedtype Route: Hashable` and `var path: NavigationPath`. Each feature area gets its own coordinator conformance with domain-specific routes and convenience methods (e.g., `showRecipeOfTheDay()` that resets path and navigates).

### 7.4 Testing Navigation

```swift
// Router is easily testable
func testNavigateToRecipe() {
    let router = Router()
    let recipe = Recipe(name: "Apple Pie")

    router.navigate(to: .recipe(recipe))

    XCTAssertEqual(router.path.count, 1)
}

func testPopToRoot() {
    let router = Router()
    router.navigate(to: .category(.desserts))
    router.navigate(to: .recipe(Recipe(name: "Apple Pie")))

    router.popToRoot()

    XCTAssertTrue(router.path.isEmpty)
}
```

---

## Testing Checklist

- [ ] Deep links navigate correctly from cold start AND while running
- [ ] Pop to root clears entire stack
- [ ] State restores on app relaunch (SceneStorage key unique per scene)
- [ ] Deleted items handled gracefully in restoration (compactMap)
- [ ] NavigationSplitView collapses correctly on iPhone (selection pushes)
- [ ] iOS 26+: Liquid Glass appearance, bottom-aligned search, tab bar minimization

---

## API Quick Reference

### NavigationStack

```swift
NavigationStack { content }
NavigationStack(path: $path) { content }
```

### NavigationSplitView

```swift
NavigationSplitView { sidebar } detail: { detail }
NavigationSplitView { sidebar } content: { content } detail: { detail }
NavigationSplitView(columnVisibility: $visibility) { ... }
```

### NavigationLink

```swift
NavigationLink(title, value: value)
NavigationLink(value: value) { label }
```

### NavigationPath

```swift
path.append(value)
path.removeLast()
path.removeLast(path.count)
path.count
path.codable  // For encoding
NavigationPath(codableRepresentation)  // For decoding
```

### Modifiers

```swift
.navigationTitle("Title")
.navigationDestination(for: Type.self) { value in View }
.searchable(text: $query)
.tabViewStyle(.sidebarAdaptable)
.tabBarMinimizeBehavior(.onScrollDown)
.backgroundExtensionEffect()
```

---

## Resources

**WWDC**: 2022-10054, 2024-10147, 2025-256, 2025-323 (Build a SwiftUI app with the new design)

**Docs**: /swiftui/tabrole/search, /swiftui/view/tabbarminimizebehavior(_:), /swiftui/view/tabviewbottomaccessory(isenabled:content:)

**Skills**: axiom-swiftui-nav, axiom-swiftui-nav-diag, axiom-swiftui-26-ref, axiom-liquid-glass, axiom-swiftui-search-ref

---

**Last Updated** Based on WWDC 2022-10054, WWDC 2024-10147, WWDC 2025-256, WWDC 2025-323 (Build a SwiftUI app with the new design)
**Platforms** iOS 16+, iPadOS 16+, macOS 13+, watchOS 9+, tvOS 16+

Related Skills

Expo UI SwiftUI

25
from ComeOnOliver/skillshub

`@expo/ui/swift-ui` package lets you use SwiftUI Views and modifiers in your app.

swiftui-view-refactor

25
from ComeOnOliver/skillshub

Refactor and review SwiftUI view files for consistent structure, dependency injection, and Observation usage. Use when asked to clean up a SwiftUI view’s layout/ordering, handle view models safely (non-optional when possible), or standardize how dependencies and @Observable state are initialized and passed.

swiftui-ui-patterns

25
from ComeOnOliver/skillshub

Best practices and example-driven guidance for building SwiftUI views and components. Use when creating or refactoring SwiftUI UI, designing tab architecture with TabView, composing screens, or needing component-specific patterns and examples.

swiftui-performance-audit

25
from ComeOnOliver/skillshub

Audit and improve SwiftUI runtime performance from code review and architecture. Use for requests to diagnose slow rendering, janky scrolling, high CPU/memory usage, excessive view updates, or layout thrash in SwiftUI apps, and to provide guidance for user-run Instruments profiling when code review alone is insufficient.

swiftui-liquid-glass

25
from ComeOnOliver/skillshub

Implement, review, or improve SwiftUI features using the iOS 26+ Liquid Glass API. Use when asked to adopt Liquid Glass in new SwiftUI UI, refactor an existing feature to Liquid Glass, or review Liquid Glass usage for correctness, performance, and design alignment.

axiom-audit

25
from ComeOnOliver/skillshub

Audit Axiom logs to identify and prioritize errors and warnings, research probable causes, and flag log smells. Use when user asks to check Axiom logs, analyze production errors, investigate log issues, or audit logging patterns.

swiftui-patterns

25
from ComeOnOliver/skillshub

SwiftUI architecture patterns, state management with @Observable, view composition, navigation, performance optimization, and modern iOS/macOS UI best practices.

Axiom — Serverless Log Analytics

25
from ComeOnOliver/skillshub

## Overview

swift-swiftui

25
from ComeOnOliver/skillshub

Standards for State Management, View Lifecycle, and Property Wrappers. Use when managing SwiftUI state, view lifecycle, or property wrappers like @State and @Binding. (triggers: **/*.swift, @State, @Binding, @ObservedObject, View, body)

ios-swiftui

25
from ComeOnOliver/skillshub

Standards for declarative UI construction and data flow in iOS. Use when building declarative SwiftUI views or managing data flow with property wrappers. (triggers: **/*View.swift, View, State, Binding, EnvironmentObject)

axiom-xctrace-ref

25
from ComeOnOliver/skillshub

Use when automating Instruments profiling, running headless performance analysis, or integrating profiling into CI/CD - comprehensive xctrace CLI reference with record/export patterns

axiom-xctest-automation

25
from ComeOnOliver/skillshub

Use when writing, running, or debugging XCUITests. Covers element queries, waiting strategies, accessibility identifiers, test plans, and CI/CD test execution patterns.