Files
agents/plugins/ui-design/skills/mobile-ios-design/references/ios-navigation.md
Seth Hobson 1e54d186fe feat(ui-design): add comprehensive UI/UX design plugin v1.0.0
New plugin covering mobile (iOS, Android, React Native) and web
applications with modern design patterns, accessibility, and design systems.

Components:
- 9 skills: design-system-patterns, accessibility-compliance, responsive-design,
  mobile-ios-design, mobile-android-design, react-native-design,
  web-component-design, interaction-design, visual-design-foundations
- 4 commands: design-review, create-component, accessibility-audit, design-system-setup
- 3 agents: ui-designer, accessibility-expert, design-system-architect

Marketplace updated:
- Version bumped to 1.3.4
- 102 agents (+3), 116 skills (+9)
2026-01-19 16:22:13 -05:00

14 KiB

iOS Navigation Patterns

NavigationStack (iOS 16+)

Basic Navigation

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

Programmatic Navigation

struct ProgrammaticNavigationView: View {
    @State private var path = NavigationPath()

    var body: some View {
        NavigationStack(path: $path) {
            VStack(spacing: 20) {
                Button("Go to Settings") {
                    path.append(Destination.settings)
                }

                Button("Go to Profile") {
                    path.append(Destination.profile)
                }

                Button("Deep Link to Item 123") {
                    path.append(Destination.settings)
                    path.append(Destination.itemDetail(id: 123))
                }
            }
            .navigationTitle("Home")
            .navigationDestination(for: Destination.self) { destination in
                switch destination {
                case .settings:
                    SettingsView()
                case .profile:
                    ProfileView()
                case .itemDetail(let id):
                    ItemDetailView(itemId: id)
                }
            }
        }
    }

    enum Destination: Hashable {
        case settings
        case profile
        case itemDetail(id: Int)
    }
}

Navigation State Persistence

struct PersistentNavigationView: View {
    @SceneStorage("navigationPath") private var pathData: Data?
    @State private var path = NavigationPath()

    var body: some View {
        NavigationStack(path: $path) {
            ContentView()
                .navigationDestination(for: Item.self) { item in
                    ItemDetailView(item: item)
                }
        }
        .onAppear {
            restorePath()
        }
        .onChange(of: path) { _, newPath in
            savePath(newPath)
        }
    }

    private func savePath(_ path: NavigationPath) {
        guard let representation = path.codable else { return }
        pathData = try? JSONEncoder().encode(representation)
    }

    private func restorePath() {
        guard let data = pathData,
              let representation = try? JSONDecoder().decode(
                NavigationPath.CodableRepresentation.self,
                from: data
              ) else { return }
        path = NavigationPath(representation)
    }
}

NavigationSplitView

Two-Column Layout

struct TwoColumnView: View {
    @State private var selectedCategory: Category?
    @State private var columnVisibility: NavigationSplitViewVisibility = .all

    var body: some View {
        NavigationSplitView(columnVisibility: $columnVisibility) {
            // Sidebar
            List(categories, selection: $selectedCategory) { category in
                NavigationLink(value: category) {
                    Label(category.name, systemImage: category.icon)
                }
            }
            .navigationTitle("Categories")
        } detail: {
            // Detail
            if let category = selectedCategory {
                CategoryDetailView(category: category)
            } else {
                ContentUnavailableView(
                    "Select a Category",
                    systemImage: "sidebar.leading"
                )
            }
        }
        .navigationSplitViewStyle(.balanced)
    }
}

Three-Column Layout

struct ThreeColumnView: View {
    @State private var selectedFolder: Folder?
    @State private var selectedDocument: Document?

    var body: some View {
        NavigationSplitView {
            // Sidebar
            List(folders, selection: $selectedFolder) { folder in
                NavigationLink(value: folder) {
                    Label(folder.name, systemImage: "folder")
                }
            }
            .navigationTitle("Folders")
        } content: {
            // Content column
            if let folder = selectedFolder {
                List(folder.documents, selection: $selectedDocument) { document in
                    NavigationLink(value: document) {
                        DocumentRow(document: document)
                    }
                }
                .navigationTitle(folder.name)
            } else {
                Text("Select a folder")
            }
        } detail: {
            // Detail column
            if let document = selectedDocument {
                DocumentDetailView(document: document)
            } else {
                ContentUnavailableView(
                    "Select a Document",
                    systemImage: "doc"
                )
            }
        }
    }
}

Sheet Navigation

Modal Sheets

struct SheetNavigationView: View {
    @State private var showSettings = false
    @State private var showNewItem = false
    @State private var editingItem: Item?

    var body: some View {
        NavigationStack {
            ContentView()
                .toolbar {
                    ToolbarItem(placement: .primaryAction) {
                        Button("Add", systemImage: "plus") {
                            showNewItem = true
                        }
                    }
                    ToolbarItem(placement: .topBarLeading) {
                        Button("Settings", systemImage: "gear") {
                            showSettings = true
                        }
                    }
                }
        }
        // Boolean-based sheet
        .sheet(isPresented: $showSettings) {
            SettingsSheet()
        }
        // Boolean-based fullscreen cover
        .fullScreenCover(isPresented: $showNewItem) {
            NewItemView()
        }
        // Item-based sheet
        .sheet(item: $editingItem) { item in
            EditItemSheet(item: item)
        }
    }
}

Sheet with Navigation

struct NavigableSheet: View {
    @Environment(\.dismiss) private var dismiss

    var body: some View {
        NavigationStack {
            Form {
                Section("General") {
                    NavigationLink("Account") {
                        AccountSettingsView()
                    }
                    NavigationLink("Notifications") {
                        NotificationSettingsView()
                    }
                }

                Section("Advanced") {
                    NavigationLink("Privacy") {
                        PrivacySettingsView()
                    }
                }
            }
            .navigationTitle("Settings")
            .toolbar {
                ToolbarItem(placement: .confirmationAction) {
                    Button("Done") {
                        dismiss()
                    }
                }
            }
        }
    }
}

Sheet Customization

struct CustomSheetView: View {
    @State private var showSheet = false

    var body: some View {
        Button("Show Sheet") {
            showSheet = true
        }
        .sheet(isPresented: $showSheet) {
            SheetContent()
                // Available detents
                .presentationDetents([
                    .medium,
                    .large,
                    .height(200),
                    .fraction(0.75)
                ])
                // Selected detent binding
                .presentationDetents([.medium, .large], selection: $selectedDetent)
                // Drag indicator visibility
                .presentationDragIndicator(.visible)
                // Corner radius
                .presentationCornerRadius(24)
                // Background interaction
                .presentationBackgroundInteraction(.enabled(upThrough: .medium))
                // Prevent interactive dismiss
                .interactiveDismissDisabled(hasUnsavedChanges)
        }
    }
}

Tab Navigation

Basic TabView

struct MainTabView: View {
    @State private var selectedTab = 0

    var body: some View {
        TabView(selection: $selectedTab) {
            HomeView()
                .tabItem {
                    Label("Home", systemImage: "house")
                }
                .tag(0)

            SearchView()
                .tabItem {
                    Label("Search", systemImage: "magnifyingglass")
                }
                .tag(1)

            ProfileView()
                .tabItem {
                    Label("Profile", systemImage: "person")
                }
                .tag(2)
                .badge(unreadCount)
        }
    }
}

Tab with Custom Badge

struct BadgedTabView: View {
    @State private var selectedTab: Tab = .home
    @State private var cartCount = 3

    enum Tab: String, CaseIterable {
        case home, search, cart, profile

        var icon: String {
            switch self {
            case .home: return "house"
            case .search: return "magnifyingglass"
            case .cart: return "cart"
            case .profile: return "person"
            }
        }
    }

    var body: some View {
        TabView(selection: $selectedTab) {
            ForEach(Tab.allCases, id: \.self) { tab in
                NavigationStack {
                    contentView(for: tab)
                }
                .tabItem {
                    Label(tab.rawValue.capitalized, systemImage: tab.icon)
                }
                .tag(tab)
                .badge(tab == .cart ? cartCount : 0)
            }
        }
    }
}

Deep Linking

URL-Based Navigation

struct DeepLinkableApp: App {
    @StateObject private var router = NavigationRouter()

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

class NavigationRouter: ObservableObject {
    @Published var path = NavigationPath()
    @Published var selectedTab: Tab = .home

    func handle(url: URL) {
        guard url.scheme == "myapp" else { return }

        switch url.host {
        case "item":
            if let id = Int(url.lastPathComponent) {
                selectedTab = .home
                path = NavigationPath()
                path.append(Destination.itemDetail(id: id))
            }
        case "settings":
            selectedTab = .profile
            path = NavigationPath()
            path.append(Destination.settings)
        default:
            break
        }
    }
}
struct UniversalLinkHandler: View {
    @EnvironmentObject private var router: NavigationRouter

    var body: some View {
        ContentView()
            .onContinueUserActivity(NSUserActivityTypeBrowsingWeb) { activity in
                guard let url = activity.webpageURL else { return }
                handleUniversalLink(url)
            }
    }

    private func handleUniversalLink(_ url: URL) {
        // Parse URL path and navigate accordingly
        let pathComponents = url.pathComponents

        if pathComponents.contains("product"),
           let idString = pathComponents.last,
           let id = Int(idString) {
            router.navigate(to: .product(id: id))
        }
    }
}

Navigation Coordinator Pattern

@MainActor
class NavigationCoordinator: ObservableObject {
    @Published var path = NavigationPath()
    @Published var sheet: Sheet?
    @Published var fullScreenCover: FullScreenCover?

    enum Sheet: Identifiable {
        case settings
        case newItem
        case editItem(Item)

        var id: String {
            switch self {
            case .settings: return "settings"
            case .newItem: return "newItem"
            case .editItem(let item): return "editItem-\(item.id)"
            }
        }
    }

    enum FullScreenCover: Identifiable {
        case onboarding
        case camera

        var id: String {
            switch self {
            case .onboarding: return "onboarding"
            case .camera: return "camera"
            }
        }
    }

    func push(_ destination: Destination) {
        path.append(destination)
    }

    func pop() {
        guard !path.isEmpty else { return }
        path.removeLast()
    }

    func popToRoot() {
        path = NavigationPath()
    }

    func present(_ sheet: Sheet) {
        self.sheet = sheet
    }

    func presentFullScreen(_ cover: FullScreenCover) {
        self.fullScreenCover = cover
    }

    func dismiss() {
        if fullScreenCover != nil {
            fullScreenCover = nil
        } else if sheet != nil {
            sheet = nil
        }
    }
}

Navigation Transitions (iOS 18+)

Custom Navigation Transitions

struct CustomTransitionView: View {
    @Namespace private var namespace

    var body: some View {
        NavigationStack {
            List(items) { item in
                NavigationLink(value: item) {
                    ItemRow(item: item)
                        .matchedTransitionSource(id: item.id, in: namespace)
                }
            }
            .navigationDestination(for: Item.self) { item in
                ItemDetailView(item: item)
                    .navigationTransition(.zoom(sourceID: item.id, in: namespace))
            }
        }
    }
}

Hero Transitions

struct HeroTransitionView: View {
    @Namespace private var animation
    @State private var selectedItem: Item?

    var body: some View {
        ZStack {
            ScrollView {
                LazyVGrid(columns: columns) {
                    ForEach(items) { item in
                        if selectedItem?.id != item.id {
                            ItemCard(item: item)
                                .matchedGeometryEffect(id: item.id, in: animation)
                                .onTapGesture {
                                    withAnimation(.spring(response: 0.3)) {
                                        selectedItem = item
                                    }
                                }
                        }
                    }
                }
            }

            if let item = selectedItem {
                ItemDetailView(item: item)
                    .matchedGeometryEffect(id: item.id, in: animation)
                    .onTapGesture {
                        withAnimation(.spring(response: 0.3)) {
                            selectedItem = nil
                        }
                    }
            }
        }
    }
}