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.

referencesdeeplinks.md

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

Deep links and navigation

Contents

Intent

Route external URLs into in-app destinations while falling back to system handling when needed.

Core patterns

  • Centralize URL handling in the router (handle(url:), handleDeepLink(url:)).
  • Inject an OpenURLAction handler that delegates to the router.
  • Use .onOpenURL for Universal Links and custom URL schemes.
  • Use .onContinueUserActivity for Handoff and other declared user activity types.
  • Let the router decide whether to navigate or open externally.

Example: router entry points

@MainActor
final class RouterPath {
  var path: [Route] = []
  var urlHandler: ((URL) -> OpenURLAction.Result)?

  func handle(url: URL) -> OpenURLAction.Result {
    guard let route = parseInternal(url), isAuthorized(route), destinationExists(route) else {
      return urlHandler?(url) ?? .systemAction
    }
    path.append(route) // Commit only after every validation passes.
    return .handled
  }

  func handleDeepLink(url: URL) -> OpenURLAction.Result {
    guard let route = parseInternal(url), isAuthorized(route), destinationExists(route) else {
      return .discarded
    }
    path.append(route)
    return .handled
  }
}

Example: attach to a root view

extension View {
  func withLinkRouter(_ router: RouterPath) -> some View {
    self
      .environment(
        \.openURL,
        OpenURLAction { url in
          router.handle(url: url)
        }
      )
      .onOpenURL { url in
        router.handleDeepLink(url: url)
      }
  }
}

Design choices to keep

  • Keep URL parsing and decision logic inside the router.
  • Avoid handling deep links in multiple places; one entry point is enough.
  • Always provide a fallback to @Environment(\.openURL) via OpenURLAction.

Pitfalls

  • Parse and validate before mutating any tab or navigation path. Invalid links must leave current navigation unchanged.
  • Avoid blocking UI while resolving remote links; use Task.

Universal Links

Universal links let iOS open your app when a user taps a standard HTTPS URL, with no custom scheme required. They require server-side configuration and an Associated Domains entitlement.

Apple App Site Association (AASA)

Host a JSON file at https://example.com/.well-known/apple-app-site-association (no file extension, served with Content-Type: application/json):

{
  "applinks": {
    "details": [
      {
        "appIDs": ["TEAMID.com.example.app"],
        "components": [
          { "/": "/items/*", "comment": "Match item detail paths" },
          { "/": "/profile/*" }
        ]
      }
    ]
  }
}

Key rules:

  • AASA must be served over HTTPS with a valid certificate; do not redirect the AASA request.
  • On iOS 14+, Apple's CDN retrieves and caches AASA files. Devices download the file on install and normally check again about once per week; there is no direct CDN invalidation. Reinstall the app or use developer mode while testing changes.
  • Use components (modern) over the legacy paths array.

Associated Domains entitlement

In your app's .entitlements file (or Signing & Capabilities in Xcode), add:

com.apple.developer.associated-domains = [
    "applinks:example.com",
    "applinks:www.example.com"
]

For development/testing, prefix with applinks:example.com?mode=developer to bypass CDN-backed retrieval.

Handling Universal Links in SwiftUI

SwiftUI receives Universal Links directly as URLs. Handle them with .onOpenURL:

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

    var body: some Scene {
        WindowGroup {
            ContentView()
                .environment(router)
                .onOpenURL { url in
                    router.handle(url: url)
                }
        }
    }
}

Docs: Supporting universal links

Custom URL Schemes

Custom URL schemes (e.g., myapp://) let other apps or websites open your app. They do not require server configuration but offer no fallback if the app is not installed.

Registering in Info.plist

Add CFBundleURLTypes to your target's Info.plist:

<key>CFBundleURLTypes</key>
<array>
  <dict>
    <key>CFBundleURLSchemes</key>
    <array>
      <string>myapp</string>
    </array>
    <key>CFBundleURLName</key>
    <string>com.example.myapp</string>
  </dict>
</array>

Handling with .onOpenURL

.onOpenURL { url in
    // url.scheme == "myapp"
    // url.host == "items", url.pathComponents for routing
    guard url.scheme == "myapp" else { return }
    router.handle(url: url)
}

Prefer universal links over custom schemes for publicly shared links — they provide a better UX (web fallback) and are more secure (domain-verified).

NSUserActivity Continuation (Handoff)

Handoff lets users start an activity on one device and continue it on another. SwiftUI provides .onContinueUserActivity and .userActivity modifiers.

Advertising an activity

struct ItemDetailView: View {
    let item: Item

    var body: some View {
        ScrollView { /* content */ }
            .userActivity("com.example.viewItem") { activity in
                activity.title = item.title
                activity.isEligibleForHandoff = true
                activity.isEligibleForSearch = true
                activity.targetContentIdentifier = item.id.uuidString
                activity.webpageURL = URL(string: "https://example.com/items/\(item.id)")
            }
    }
}

Receiving a continued activity

.onContinueUserActivity("com.example.viewItem") { activity in
    guard let id = activity.targetContentIdentifier else { return }
    router.navigate(to: .item(id: id))
}

Key rules:

  • Activity types must be declared in Info.plist under NSUserActivityTypes.
  • Set isEligibleForHandoff = true and optionally isEligibleForSearch / isEligibleForPrediction.
  • Provide a webpageURL as fallback when the app is not installed on the receiving device.
  • Do not use the browsing-web user activity hook as the primary SwiftUI Universal Link handler; use .onOpenURL for Universal Links.

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.