Skip to content
Mobile
Skill

/swiftui-navigation

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

From plugin
swift-ios-skills
1.3k86 skills1 MCP

Claude Code

$ npx -y skills add dpearson2699/swift-ios-skills --skill swiftui-navigation --agent claude-code

SKILL.md

swiftui-navigation.SKILL.md
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.3. 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 typed `[Route]` binding for programmatic push navigation. Define routes as a `Hashable` enum and map them with `.navigationDestination(for:)`; this keeps the path compile-time checked. Use `NavigationPath` only when one stack must hold heterogeneous route value types.

enum Route: Hashable {
    case item(id: Item.ID)
}

struct ContentView: View {
    @State private var path: [Route] = []
    let items: [Item]

    var body: some View {
        NavigationStack(path: $path) {
            List(items) { item in
                NavigationLink(value: Route.item(id: item.id)) {
                    ItemRow(item: item)
                }
            }
            .navigationDestination(for: Route.self) { route in
                switch route {
                case .item(let id):
                    DetailView(itemID: id)
                }
            }
            .navigationTitle("Items")
        }
    }
}

**Programmatic navigation:**

path.append(.item(id: item.id))  // Push
path.removeLast()                // Pop one
path = []                        // 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](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.

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:

@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.

@State private var selectedItem: Item?

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

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

.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 protection:** On iOS/iPadOS, use `.interactiveDismissDisabled(hasUnsavedChanges)` and provide explicit Save/Discard actions inside the sheet. On macOS 15+, use `.dismissalConfirmationDialog("Discard?", shouldPresent: hasUnsavedChanges)` for window dismissal confirmation. Route every programmatic close through the same save/validate/discard gate; `interactiveDismissDisabled` guards interactive dismissal only.

**Enum-driven sheet routing:** Define a `SheetDestination` enum that is `Identifiable`, store it on the router, and map it with a shared view modif

Read more
@dpearson2699
Ships withswift-ios-skills

86 agent skills optimized for iOS 26+ development with Swift 6.3 and modern Apple frameworks.

1.3k86 skills1 MCP
Get the whole plugin

Stars on GitHub

Momentum

stars in total
1.3k
Star history of dpearson2699/swift-ios-skills on GitHub, a chart by star-history.com

Other skills on swift-ios-skills.

alarmkit

alarmkit

Implement AlarmKit alarms and countdown timers for iOS and iPadOS with Lock Screen, Dynamic Island, StandBy, and paired Apple…

app-clips

app-clips

Build iOS App Clips with invocation URLs, App Clip Codes, NFC, QR codes, Safari banners, Maps, Messages, target setup, App Store…

More skills from other plugins.

deep-code-review

deep-code-review

In-depth design-focused code review - understands codebase context before evaluating PR changes, posts structured feedback to…

expo1d53k
@expo@expoInstall Skill
modlens

modlens

Plug-in vision for text-only models. Hard rule: when a file path or URL with an image extension (.png, .jpg, .jpeg, .webp, .gif,…

modlens2w4.6k
@liustack@liustackInstall Skill
appllama-app-design-skill

appllama-app-design-sk…

Build native-feeling, benchmark-quality mobile app screens (Expo / React Native). Use when designing or implementing any mobile…

appllama2mo3.9k
@appllama@appllamaInstall Skill

Questions about swiftui-navigation.

Which coding agents can use swiftui-navigation?
swiftui-navigation installs in Claude Code, Codex, Cursor, Gemini CLI, GitHub Copilot, OpenCode and Windsurf with the skills CLI, using each agent's flag shown on this page. Flowy's auto-invocation, which fires a skill when your prompt matches, runs in Claude Code only.