All skills
dpearson2699 avatar

/swiftui-navigation

@cf3fe87

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.

Use this Skill: https://skilld.dev/gh/dpearson2699/swift-ios-skills/swiftui-navigation

This session only. Nothing lands on disk.

referencesnavigationstack.md

≈1.3k tokens on demand. Your agent reads this file only when SKILL.md points to it.

NavigationStack

Contents

Intent

Use this pattern for programmatic navigation and deep links, especially when each tab needs an independent navigation history. The key idea is one NavigationStack per tab, each with its own path binding and router object.

Core architecture

  • Define a route enum that is Hashable and represents all destinations.
  • Create a lightweight router (or use a library such as https://github.com/Dimillian/AppRouter) that owns the path and any sheet state.
  • Each tab owns its own router instance and binds NavigationStack(path:) to it.
  • Inject the router into the environment so child views can navigate programmatically.
  • Centralize destination mapping with a single navigationDestination(for:) block (or a withAppRouter() modifier).

Example: custom router with per-tab stack

@MainActor
@Observable
final class RouterPath {
  var path: [Route] = []
  var presentedSheet: SheetDestination?

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

  func reset() {
    path = []
  }
}

enum Route: Hashable {
  case account(id: String)
  case status(id: String)
}

@MainActor
struct TimelineTab: View {
  @State private var routerPath = RouterPath()

  var body: some View {
    NavigationStack(path: $routerPath.path) {
      TimelineView()
        .navigationDestination(for: Route.self) { route in
          switch route {
          case .account(let id): AccountView(id: id)
          case .status(let id): StatusView(id: id)
          }
        }
    }
    .environment(routerPath)
  }
}

Example: centralized destination mapping

Use a shared view modifier to avoid duplicating route switches across screens.

extension View {
  func withAppRouter() -> some View {
    navigationDestination(for: Route.self) { route in
      switch route {
      case .account(let id):
        AccountView(id: id)
      case .status(let id):
        StatusView(id: id)
      }
    }
  }
}

Then apply it once per stack:

NavigationStack(path: $routerPath.path) {
  TimelineView()
    .withAppRouter()
}

Example: binding per tab (tabs with independent history)

@MainActor
struct TabsView: View {
  @State private var selectedTab: AppTab = .timeline
  @State private var timelineRouter = RouterPath()
  @State private var notificationsRouter = RouterPath()

  var body: some View {
    TabView(selection: $selectedTab) {
      Tab("Timeline", systemImage: "text.bubble", value: .timeline) {
        TimelineTab(router: timelineRouter)
      }
      Tab("Notifications", systemImage: "bell", value: .notifications) {
        NotificationsTab(router: notificationsRouter)
      }
    }
  }
}

Example: generic tabs with per-tab NavigationStack

Use this when tabs are built from data and each needs its own path without hard-coded names.

@MainActor
struct TabsView: View {
  @State private var selectedTab: AppTab = .timeline
  @State private var tabRouter = TabRouter()

  var body: some View {
    TabView(selection: $selectedTab) {
      ForEach(AppTab.allCases) { tab in
        Tab(value: tab) {
          NavigationStack(path: tabRouter.binding(for: tab)) {
            tab.makeContentView()
          }
          .environment(tabRouter.router(for: tab))
        } label: {
          tab.label
        }
      }
    }
  }
}

@MainActor
@Observable
final class TabRouter {
  private var routers: [AppTab: RouterPath] = [:]

  func router(for tab: AppTab) -> RouterPath {
    if let router = routers[tab] { return router }
    let router = RouterPath()
    routers[tab] = router
    return router
  }

  func binding(for tab: AppTab) -> Binding<[Route]> {
    let router = router(for: tab)
    return Binding(get: { router.path }, set: { router.path = $0 })
  }
}

Design choices to keep

  • One NavigationStack per tab to preserve independent history.
  • A single source of truth for navigation state (RouterPath or library router).
  • Use navigationDestination(for:) to map routes to views.
  • Reset the path when app context changes (account switch, logout, etc.).
  • Inject the router into the environment so child views can navigate and present sheets without prop-drilling.
  • Keep sheet presentation state on the router if you want a single place to manage modals.

Pitfalls

  • Do not share one path across all tabs unless you want global history.
  • Ensure route identifiers are stable and Hashable.
  • Avoid storing view instances in the path; store lightweight route data instead.
  • If using a router object, keep it outside other @Observable objects to avoid nested observation.

Source: SKILL.md on GitHub

No alerts17d4 checks · Risk SAFE
  • Gen Agent Trust Hub17d

    The skill provides comprehensive documentation and development patterns for SwiftUI navigation targeting iOS 26 and Swift 6.3. It includes best practices for NavigationStack, sheets, and deep links without any security violations.

  • Socket17d

    No alerts

  • Snyk17d

    Risk: LOW · No issues

  • ZeroLeaks5mo

    Score: 93/100 · 2 sections analyzed

Signed by skilld at cf3fe87. This ties the file your Agent reads to that commit on GitHub. It does not review the instructions.

Last checked against GitHub 2 days ago.

Steadyupdated 3 months ago
  • swiftui
  • ios
  • navigation
  • routing
  • deep-linking
  • tab-navigation
  • swift
  • sheet-presentation

README badge

README badge for dpearson2699/swift-ios-skills/swiftui-navigation

Implement SwiftUI navigation patterns for iOS 17+ including NavigationStack for push navigation, NavigationSplitView for multi-column layouts, sheet presentation, tab-based navigation with the Tab API, and deep linking via universal links and custom URL schemes. Includes router patterns for programmatic routing and centralized sheet/destination mapping.

Generated from the current SKILL.md.

Does this skill work with iOS versions before iOS 26?
Most patterns are backward-compatible to iOS 17. NavigationStack, NavigationSplitView, sheet presentation, and tab-based navigation work on iOS 17+. iOS 26-specific features like Tab(role: .search), .presentationSizing, and .dismissalConfirmationDialog are noted separately in the skill.
How do I handle deep linking with custom URL schemes?
Register schemes in Info.plist under CFBundleURLTypes, then parse and route URLs in a centralized router object using .onOpenURL. The skill recommends universal links over custom schemes for public links because they provide web fallback and domain verification.
Should each tab have its own NavigationStack?
Yes. Each tab must have an independent NavigationStack with its own NavigationPath to avoid sharing navigation state across tabs, which is a common mistake noted in the skill.
What's the difference between .sheet(item:) and .sheet(isPresented:)?
Use .sheet(item:) when state represents a selected model, and .sheet(isPresented:) for simple boolean flags. The skill prefers .sheet(item:) because it binds to actual data rather than a presentation flag.

Generated from the current SKILL.md. These answers refresh after source changes.