All skills
wshobson avatar

/mobile-ios-design

@47a5dbc
by Seth Hobsonwshobson/agents40k stars
4,281

Master iOS Human Interface Guidelines and SwiftUI patterns for building native iOS apps. Use when designing iOS interfaces, implementing SwiftUI views, or ensuring apps follow Apple's design principles.

Use this Skill: https://skilld.dev/gh/wshobson/agents/mobile-ios-design

This session only. Nothing lands on disk.

referencesios-navigation.md

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

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 (iOS 18+)

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

    var body: some View {
        TabView(selection: $selectedTab) {
            Tab("Home", systemImage: "house", value: 0) {
                HomeView()
            }

            Tab("Search", systemImage: "magnifyingglass", value: 1) {
                SearchView()
            }

            Tab("Profile", systemImage: "person", value: 2) {
                ProfileView()
            }
            .badge(unreadCount)
        }
    }
}

Tab with Custom Badge (iOS 18+)

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

    enum AppTab: 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(AppTab.allCases, id: \.self) { tab in
                Tab(tab.rawValue.capitalized, systemImage: tab.icon, value: tab) {
                    NavigationStack {
                        contentView(for: 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
        }
    }
}

Universal Links

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
                        }
                    }
            }
        }
    }
}

Source: SKILL.md on GitHub

No alerts16d5 checks · Risk SAFE
  • Gen Agent Trust Hub16d

    The skill provides a comprehensive collection of iOS Human Interface Guidelines (HIG) patterns and SwiftUI code snippets for native application design. No security risks, malicious patterns, or unsafe dependencies were identified.

  • Socket16d

    No alerts

  • Snyk16d

    Risk: LOW · No issues

  • Runlayer6mo

    4 files scanned · No issues

  • ZeroLeaks5mo

    Score: 93/100 · 2 sections analyzed

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

Last checked against GitHub 3 days ago.

Activeupdated 7 months ago
  • ios
  • swiftui
  • hig
  • mobile-design
  • navigation
  • accessibility
  • sf-symbols
  • adaptive-layout

README badge

README badge for wshobson/agents/mobile-ios-design

Instructs Claude on iOS Human Interface Guidelines and SwiftUI patterns for building native iOS apps. Covers layout systems (VStack, HStack, LazyVGrid), navigation patterns (NavigationStack, TabView), SF Symbols, Dynamic Type, semantic colors, and accessibility best practices specific to iOS design.

Generated from the current SKILL.md.

Does this skill cover both iPhone and iPad design?
Yes. The skill covers iOS Human Interface Guidelines, SwiftUI patterns for both iPhone and iPad, including adaptive layouts and iPad-specific considerations like multitasking and split view support.
What SwiftUI version does this assume?
The skill includes examples targeting iOS 16+ (NavigationStack) and iOS 18+ (new TabView API), but also covers older patterns. Code examples note version requirements where they apply.
Does this cover accessibility?
Yes. The skill includes guidance on accessibility modifiers, Dynamic Type support, semantic fonts, VoiceOver testing, and SF Symbols for consistent screen reader experiences.
Will this help me follow Apple's design guidelines?
Yes. The skill is built on Apple's Human Interface Guidelines principles (Clarity, Deference, Depth) and teaches best practices like semantic colors, system materials, and platform-specific patterns for iOS and iPadOS.
Does this include visionOS design patterns?
The skill mentions visionOS briefly as a platform consideration but does not provide detailed visionOS-specific patterns or spatial computing guidance.

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