All skills

Comprehensive iOS development guidance including Swift best practices, SwiftUI patterns, UI/UX review against HIG, and app planning. Use for iOS code review, best practices, accessibility audits, or planning new iOS apps.

  • 38 files
  • 438.7 KB
  • Updated 3 months ago
  • GitHub

Use this Skill: https://skilld.dev/gh/rshankras/claude-code-apple-skills/ios

This session only. Nothing lands on disk.

navigation-patternsnavigation-stack.md

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

NavigationStack Patterns

The primary navigation container for hierarchical drill-down interfaces. Replaced NavigationView in iOS 16.

Core Pattern

struct ContentView: View {
    var body: some View {
        NavigationStack {
            List(items) { item in
                NavigationLink(value: item) {
                    ItemRow(item: item)
                }
            }
            .navigationTitle("Items")
            .navigationDestination(for: Item.self) { item in
                ItemDetailView(item: item)
            }
        }
    }
}

Key elements:

  • NavigationStack wraps the root view
  • NavigationLink(value:) pushes a value onto the stack
  • .navigationDestination(for:) maps value types to destination views
  • Destination registration can be on any view inside the stack

NavigationLink

Value-Based Links (Modern)

// ✅ Modern — type-safe, works with programmatic navigation
NavigationLink(value: item) {
    Label(item.name, systemImage: item.icon)
}

// Register the destination once
.navigationDestination(for: Item.self) { item in
    ItemDetailView(item: item)
}

Destination-Based Links (Legacy)

// ❌ Legacy — no programmatic navigation support, eager view creation
NavigationLink(destination: ItemDetailView(item: item)) {
    Label(item.name, systemImage: item.icon)
}

Why? Destination-based links create the destination view immediately (even before tap). Value-based links defer view creation and integrate with NavigationPath.

Multiple Destination Types

Register multiple .navigationDestination modifiers for different types:

NavigationStack {
    List {
        Section("People") {
            ForEach(people) { person in
                NavigationLink(value: person) {
                    PersonRow(person: person)
                }
            }
        }
        Section("Places") {
            ForEach(places) { place in
                NavigationLink(value: place) {
                    PlaceRow(place: place)
                }
            }
        }
    }
    .navigationDestination(for: Person.self) { person in
        PersonDetailView(person: person)
    }
    .navigationDestination(for: Place.self) { place in
        PlaceDetailView(place: place)
    }
}

Nested Navigation (Multi-Level Drill-Down)

Each pushed view can have its own NavigationLink values. They all share the same stack:

struct CategoryView: View {
    let category: Category

    var body: some View {
        List(category.items) { item in
            NavigationLink(value: item) {
                ItemRow(item: item)
            }
        }
        .navigationTitle(category.name)
    }
}

No need for nested NavigationStack — there is one stack at the root and all pushed views participate in it.

Common Mistakes

Nesting NavigationStacks

// ❌ Double navigation bar, broken back button
NavigationStack {
    NavigationStack {
        Text("Hello")
    }
}

// ❌ NavigationStack inside a pushed view
struct DetailView: View {
    var body: some View {
        NavigationStack {  // Wrong — creates a second stack
            Text("Detail")
        }
    }
}

// ✅ One NavigationStack at the root, pushed views are plain
struct DetailView: View {
    var body: some View {
        Text("Detail")
            .navigationTitle("Detail")
    }
}

Missing navigationDestination

// ❌ NavigationLink pushes a value but no destination registered — tap does nothing
NavigationStack {
    NavigationLink(value: item) { Text(item.name) }
    // Forgot .navigationDestination(for: Item.self) { ... }
}

navigationDestination Outside NavigationStack

// ❌ Destination registered outside the stack — never matched
VStack {
    NavigationStack {
        NavigationLink(value: item) { Text(item.name) }
    }
    .navigationDestination(for: Item.self) { item in  // Outside stack
        DetailView(item: item)
    }
}

// ✅ Register inside the stack
NavigationStack {
    NavigationLink(value: item) { Text(item.name) }
        .navigationDestination(for: Item.self) { item in
            DetailView(item: item)
        }
}

Duplicate Destination Registrations

// ❌ Two registrations for same type — undefined behavior
NavigationStack {
    content
        .navigationDestination(for: Item.self) { item in ViewA(item: item) }
        .navigationDestination(for: Item.self) { item in ViewB(item: item) }
}

// ✅ One registration per type, use an enum for multiple routes
enum Route: Hashable {
    case viewA(Item)
    case viewB(Item)
}

NavigationStack {
    content
        .navigationDestination(for: Route.self) { route in
            switch route {
            case .viewA(let item): ViewA(item: item)
            case .viewB(let item): ViewB(item: item)
            }
        }
}

navigationDestination with isPresented

For conditional navigation (push based on a boolean):

@State private var showSettings = false

NavigationStack {
    Button("Settings") { showSettings = true }
        .navigationDestination(isPresented: $showSettings) {
            SettingsView()
        }
}

This pushes SettingsView when showSettings becomes true and pops it when it becomes false.

NavigationStack with Explicit Path

For programmatic control, pass a NavigationPath or typed array:

// Type-erased path (supports mixed types)
@State private var path = NavigationPath()

NavigationStack(path: $path) {
    RootView()
        .navigationDestination(for: Item.self) { item in
            ItemView(item: item)
        }
        .navigationDestination(for: Category.self) { cat in
            CategoryView(category: cat)
        }
}

// Typed path (single type only)
@State private var path: [Item] = []

NavigationStack(path: $path) {
    RootView()
        .navigationDestination(for: Item.self) { item in
            ItemView(item: item)
        }
}

See programmatic-navigation.md for full programmatic navigation patterns.

Toolbar and Navigation Bar

NavigationStack {
    ContentView()
        .navigationTitle("Home")
        .navigationBarTitleDisplayMode(.large)  // .large, .inline, .automatic
        .toolbar {
            ToolbarItem(placement: .primaryAction) {
                Button("Add", systemImage: "plus") { }
            }
            ToolbarItem(placement: .topBarLeading) {
                EditButton()
            }
        }
        .toolbarRole(.editor)  // Hides back button title, shows only chevron
}

Searchable Navigation

NavigationStack {
    List(filteredItems) { item in
        NavigationLink(value: item) { ItemRow(item: item) }
    }
    .searchable(text: $searchText, prompt: "Search items")
    .navigationTitle("Items")
    .navigationDestination(for: Item.self) { item in
        ItemDetailView(item: item)
    }
}

Checklist

  • Using NavigationStack (not deprecated NavigationView)
  • Using NavigationLink(value:) with .navigationDestination(for:)
  • No nested NavigationStack in pushed views
  • One .navigationDestination per type per stack
  • Destination registered inside the NavigationStack scope
  • Using NavigationPath if programmatic navigation is needed
  • Navigation title set on inner views (not on NavigationStack itself)

Source: SKILL.md on GitHub

No third-party reports yet.

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

Last checked against GitHub 2 months ago.

Steadyupdated 3 months ago
What it can do
Reads files Network
last_verified
2026-07-16
review_by
2027-06-22
os_version
iOS 27 / macOS 27
All 4 allowed tools
ReadGlobGrepWebFetch

README badge

README badge for rshankras/claude-code-apple-skills/ios