agentleFS
moasqmoasq/ios-dev-agent/.agents/skills/navigation/SKILL.md

navigation

Skill4 starsChanged 6 months agoMITView on GitHub

Use when implementing or debugging SwiftUI navigation: NavigationStack, NavigationSplitView, TabView, sheets, fullScreenCover, type-safe routes, deep links, and programmatic navigation.

12 other projects use a version of this.

Adopted by

Most recently taken up by

Used with

Projects with it also use testing.

Other files people use alongside this one.

Files found in the same public projects as a version of this one, more often than they turn up anywhere else. Each project counts once, large collections count for less, and this file's own repository is left out.
  • NVIDIASkill

    testing

    Run Python and Rust tests for physicsnemo-curator using uv + pytest with coverage reporting via pytest-cov, and cargo-nextest for Rust tests. Includes benchmark workflows with pytest-benchmark and criterion.

    Also in 5 projects that use this

The file

We split this skill into the rules it gives an agent

9 rules. 0 are common, 3 are distinctive, and 6 are very specific to this project.

  • 0

    Common

    Most of the 9 edited versions we read keep these.

  • 3

    Distinctive

    This author's own choices. Worth a look.

  • 6

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Distinctive:

    Use for hierarchical navigation with a back button:

    line 18
  2. Distinctive:

    Use when the app has 3+ distinct, peer-level sections:

    line 35
  3. Distinctive:

    Use for immersive content that should cover the entire screen:

    line 74
  4. Very specific:

    Always use navigationDestination(for:) for type-safe routing:

    line 85
  5. Very specific:

    Always use .confirmationDialog — becomes popover on iPad, action sheet on iPhone:

    line 377
  6. Very specific:

    ALWAYS use NavigationSplitView for list-detail flows — never bare NavigationStack on iPad

    line 387
  7. Very specific:

    ALWAYS provide a detail placeholder (ContentUnavailableView) for iPad empty state

    line 388
  8. Very specific:

    Use .popover() for contextual actions — SwiftUI adapts automatically

    line 389
  9. Very specific:

    NEVER check UIDevice.current.userInterfaceIdiom for navigation decisions

    line 390

“Common” is measured: kept by at least 6 in 10 of the 9 edited versions of this file we could read. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

Navigation Pattern Rules

Pattern Selection Guide

PatternWhen to Use
NavigationStackHierarchical drill-down (list → detail → edit)
TabView with Tab API3+ distinct top-level peer sections
.sheet(item:)Creation forms, secondary actions, settings
.fullScreenCoverImmersive experiences (media player, onboarding)
NavigationStack + .sheetMost MVPs with 2-4 features

NavigationStack

Use for hierarchical navigation with a back button:

NavigationStack {
    List(items) { item in
        NavigationLink(value: item) {
            ItemRow(item: item)
        }
    }
    .navigationDestination(for: Item.self) { item in
        ItemDetailView(item: item)
    }
    .navigationTitle("Items")
}

TabView with Tab API

Use when the app has 3+ distinct, peer-level sections:

TabView {
    Tab("Home", systemImage: "house") {
        HomeView()
    }
    Tab("Search", systemImage: "magnifyingglass") {
        SearchView()
    }
    Tab("Profile", systemImage: "person") {
        ProfileView()
    }
}

Sheets

Choose the right sheet variant based on context:

  • .sheet(item:) — for editing or viewing an existing item (the item drives the sheet)
  • .sheet(isPresented:) — acceptable for creation forms and simple actions (no item yet)
// Editing/viewing an existing item — use item-driven
@State private var editingItem: Item?

.sheet(item: $editingItem) { item in
    EditItemView(item: item)
}

// Creating a new item — isPresented is fine
@State private var showAddItem = false

.sheet(isPresented: $showAddItem) {
    AddItemView()
}

Full Screen Cover

Use for immersive content that should cover the entire screen:

@State private var showOnboarding = false

.fullScreenCover(isPresented: $showOnboarding) {
    OnboardingView()
}

Type-Safe Routing

Always use navigationDestination(for:) for type-safe routing:

// Define route types
.navigationDestination(for: Note.self) { note in
    NoteDetailView(note: note)
}
.navigationDestination(for: Category.self) { category in
    CategoryView(category: category)
}

NavigationStack — Type-Safe Navigation

struct ContentView: View {
    var body: some View {
        NavigationStack {
            List {
                NavigationLink("Profile", value: Route.profile)
                NavigationLink("Settings", value: Route.settings)
            }
            .navigationDestination(for: Route.self) { route in
                switch route {
                case .profile:
                    ProfileView()
                case .settings:
                    SettingsView()
                }
            }
        }
    }
}

enum Route: Hashable {
    case profile
    case settings
}

Programmatic Navigation

struct ContentView: View {
    @State private var navigationPath = NavigationPath()

    var body: some View {
        NavigationStack(path: $navigationPath) {
            List {
                Button("Go to Detail") {
                    navigationPath.append(DetailRoute.item(id: 1))
                }
            }
            .navigationDestination(for: DetailRoute.self) { route in
                switch route {
                case .item(let id):
                    ItemDetailView(id: id)
                }
            }
        }
    }
}

enum DetailRoute: Hashable {
    case item(id: Int)
}

Sheet Patterns

Item-Driven Sheets (Preferred)

// Good - item-driven
@State private var selectedItem: Item?

var body: some View {
    List(items) { item in
        Button(item.name) {
            selectedItem = item
        }
    }
    .sheet(item: $selectedItem) { item in
        ItemDetailSheet(item: item)
    }
}

// Avoid - boolean flag requires separate state
@State private var showSheet = false
@State private var selectedItem: Item?

Why: .sheet(item:) automatically handles presentation state and avoids optional unwrapping.

Sheets Own Their Actions

Sheets should handle their own dismiss and actions internally.

struct EditItemSheet: View {
    @Environment(\.dismiss) private var dismiss
    @Environment(DataStore.self) private var store

    let item: Item
    @State private var name: String
    @State private var isSaving = false

    init(item: Item) {
        self.item = item
        _name = State(initialValue: item.name)
    }

    var body: some View {
        NavigationStack {
            Form {
                TextField("Name", text: $name)
            }
            .navigationTitle("Edit Item")
            .toolbar {
                ToolbarItem(placement: .cancellationAction) {
                    Button("Cancel") { dismiss() }
                }
                ToolbarItem(placement: .confirmationAction) {
                    Button(isSaving ? "Saving..." : "Save") {
                        Task { await save() }
                    }
                    .disabled(isSaving || name.isEmpty)
                }
            }
        }
    }

    private func save() async {
        isSaving = true
        await store.updateItem(item, name: name)
        dismiss()
    }
}

Popover

@State private var showPopover = false

Button("Show Popover") {
    showPopover = true
}
.popover(isPresented: $showPopover) {
    PopoverContentView()
        .presentationCompactAdaptation(.popover)
}

Alert with Actions

.alert("Delete Item?", isPresented: $showAlert) {
    Button("Delete", role: .destructive) { deleteItem() }
    Button("Cancel", role: .cancel) { }
} message: {
    Text("This action cannot be undone.")
}

Confirmation Dialog

.confirmationDialog("Choose an option", isPresented: $showDialog) {
    Button("Option 1") { handleOption1() }
    Button("Option 2") { handleOption2() }
    Button("Cancel", role: .cancel) { }
}

iPad-Specific Patterns

NavigationSplitView (PRIMARY pattern for iPad)

For ANY list-detail flow, use NavigationSplitView. It shows sidebar+detail on iPad and auto-collapses to NavigationStack on iPhone.

Two-column (most common)

@State private var selectedItem: Item?

NavigationSplitView {
    List(items, selection: $selectedItem) { item in
        NavigationLink(value: item) {
            ItemRow(item: item)
        }
    }
    .navigationTitle("Items")
} detail: {
    if let selectedItem {
        ItemDetailView(item: selectedItem)
    } else {
        ContentUnavailableView("Select an Item", systemImage: "doc")
    }
}

Three-column (sidebar categories)

@State private var selectedCategory: Category?
@State private var selectedItem: Item?

NavigationSplitView {
    List(categories, selection: $selectedCategory) { category in
        Label(category.name, systemImage: category.icon)
    }
    .navigationTitle("Categories")
} content: {
    if let selectedCategory {
        List(selectedCategory.items, selection: $selectedItem) { item in
            NavigationLink(value: item) { ItemRow(item: item) }
        }
        .navigationTitle(selectedCategory.name)
    }
} detail: {
    if let selectedItem {
        ItemDetailView(item: selectedItem)
    } else {
        ContentUnavailableView("Select an Item", systemImage: "doc")
    }
}

Column visibility control

@State private var columnVisibility: NavigationSplitViewVisibility = .automatic

NavigationSplitView(columnVisibility: $columnVisibility) {
    SidebarView()
} detail: {
    DetailView()
}

TabView + NavigationSplitView

For apps with 3+ top-level sections on iPad, use TabView wrapping NavigationSplitView inside each tab:

TabView {
    Tab("Library", systemImage: "books.vertical") {
        NavigationSplitView {
            LibraryListView()
        } detail: {
            ContentUnavailableView("Select a Book", systemImage: "book")
        }
    }
    Tab("Search", systemImage: "magnifyingglass") {
        SearchView()
    }
}

iPad Presentation Differences

Popovers (auto-adaptive)

.popover(isPresented: $showOptions) {
    OptionsView()
        .frame(minWidth: 250, minHeight: 300)
}
  • iPad (regular width): floating popover anchored to source view
  • iPhone (compact width): automatically becomes a sheet

Sheets on iPad

.sheet(isPresented: $showSheet) {
    SheetContent()
        .presentationDetents([.medium, .large])
}
  • iPad wide: centered floating modal (detents are ignored)
  • iPad narrow/split: edge-attached (detents are respected)

Confirmation dialogs

Always use .confirmationDialog — becomes popover on iPad, action sheet on iPhone:

.confirmationDialog("Options", isPresented: $showDialog) {
    Button("Edit") { }
    Button("Delete", role: .destructive) { }
    Button("Cancel", role: .cancel) { }
}

iPad Navigation Rules

  1. ALWAYS use NavigationSplitView for list-detail flows — never bare NavigationStack on iPad
  2. ALWAYS provide a detail placeholder (ContentUnavailableView) for iPad empty state
  3. Use .popover() for contextual actions — SwiftUI adapts automatically
  4. NEVER check UIDevice.current.userInterfaceIdiom for navigation decisions

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.