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.

referencesswiftui-components.md

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

SwiftUI Component Library

Lists and Collections

Basic List

struct ItemListView: View {
    @State private var items: [Item] = []

    var body: some View {
        List {
            ForEach(items) { item in
                ItemRow(item: item)
            }
            .onDelete(perform: deleteItems)
            .onMove(perform: moveItems)
        }
        .listStyle(.insetGrouped)
        .refreshable {
            await loadItems()
        }
    }

    private func deleteItems(at offsets: IndexSet) {
        items.remove(atOffsets: offsets)
    }

    private func moveItems(from source: IndexSet, to destination: Int) {
        items.move(fromOffsets: source, toOffset: destination)
    }
}

Sectioned List

struct SectionedListView: View {
    let groupedItems: [String: [Item]]

    var body: some View {
        List {
            ForEach(groupedItems.keys.sorted(), id: \.self) { key in
                Section(header: Text(key)) {
                    ForEach(groupedItems[key] ?? []) { item in
                        ItemRow(item: item)
                    }
                }
            }
        }
        .listStyle(.sidebar)
    }
}

Search Integration

struct SearchableListView: View {
    @State private var searchText = ""
    @State private var items: [Item] = []

    var filteredItems: [Item] {
        if searchText.isEmpty {
            return items
        }
        return items.filter { $0.name.localizedCaseInsensitiveContains(searchText) }
    }

    var body: some View {
        NavigationStack {
            List(filteredItems) { item in
                ItemRow(item: item)
            }
            .searchable(text: $searchText, prompt: "Search items")
            .searchSuggestions {
                ForEach(searchSuggestions, id: \.self) { suggestion in
                    Text(suggestion)
                        .searchCompletion(suggestion)
                }
            }
            .navigationTitle("Items")
        }
    }
}

Forms and Input

Settings Form

struct SettingsView: View {
    @AppStorage("notifications") private var notificationsEnabled = true
    @AppStorage("soundEnabled") private var soundEnabled = true
    @State private var selectedTheme = Theme.system
    @State private var username = ""

    var body: some View {
        Form {
            Section("Account") {
                TextField("Username", text: $username)
                    .textContentType(.username)
                    .autocorrectionDisabled()
            }

            Section("Preferences") {
                Toggle("Enable Notifications", isOn: $notificationsEnabled)
                Toggle("Sound Effects", isOn: $soundEnabled)

                Picker("Theme", selection: $selectedTheme) {
                    ForEach(Theme.allCases) { theme in
                        Text(theme.rawValue).tag(theme)
                    }
                }
            }

            Section("About") {
                LabeledContent("Version", value: "1.0.0")

                Link(destination: URL(string: "https://example.com/privacy")!) {
                    Text("Privacy Policy")
                }
            }
        }
        .navigationTitle("Settings")
    }
}

Custom Input Fields

struct ValidatedTextField: View {
    let title: String
    @Binding var text: String
    let validation: (String) -> Bool

    @State private var isValid = true
    @FocusState private var isFocused: Bool

    var body: some View {
        VStack(alignment: .leading, spacing: 4) {
            Text(title)
                .font(.caption)
                .foregroundStyle(.secondary)

            TextField(title, text: $text)
                .textFieldStyle(.roundedBorder)
                .focused($isFocused)
                .overlay(
                    RoundedRectangle(cornerRadius: 8)
                        .stroke(borderColor, lineWidth: 1)
                )
                .onChange(of: text) { _, newValue in
                    isValid = validation(newValue)
                }

            if !isValid && !text.isEmpty {
                Text("Invalid input")
                    .font(.caption)
                    .foregroundStyle(.red)
            }
        }
    }

    private var borderColor: Color {
        if isFocused {
            return isValid ? .blue : .red
        }
        return .clear
    }
}

Buttons and Actions

Button Styles

// Primary filled button
Button("Continue") {
    // action
}
.buttonStyle(.borderedProminent)
.controlSize(.large)

// Secondary button
Button("Cancel") {
    // action
}
.buttonStyle(.bordered)

// Destructive button
Button("Delete", role: .destructive) {
    // action
}
.buttonStyle(.bordered)

// Custom button style
struct ScaleButtonStyle: ButtonStyle {
    func makeBody(configuration: Configuration) -> some View {
        configuration.label
            .scaleEffect(configuration.isPressed ? 0.95 : 1.0)
            .animation(.easeInOut(duration: 0.1), value: configuration.isPressed)
    }
}

Menu and Context Menu

// Menu button
Menu {
    Button("Edit", systemImage: "pencil") { }
    Button("Duplicate", systemImage: "doc.on.doc") { }
    Divider()
    Button("Delete", systemImage: "trash", role: .destructive) { }
} label: {
    Image(systemName: "ellipsis.circle")
}

// Context menu on any view
Text("Long press me")
    .contextMenu {
        Button("Copy", systemImage: "doc.on.doc") { }
        Button("Share", systemImage: "square.and.arrow.up") { }
    } preview: {
        ItemPreviewView()
    }

Sheets and Modals

Sheet Presentation

struct ParentView: View {
    @State private var showSettings = false
    @State private var selectedItem: Item?

    var body: some View {
        VStack {
            Button("Settings") {
                showSettings = true
            }
        }
        .sheet(isPresented: $showSettings) {
            SettingsSheet()
                .presentationDetents([.medium, .large])
                .presentationDragIndicator(.visible)
        }
        .sheet(item: $selectedItem) { item in
            ItemDetailSheet(item: item)
                .presentationDetents([.height(300), .large])
                .presentationCornerRadius(24)
        }
    }
}

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

    var body: some View {
        NavigationStack {
            SettingsContent()
                .toolbar {
                    ToolbarItem(placement: .confirmationAction) {
                        Button("Done") {
                            dismiss()
                        }
                    }
                }
        }
    }
}

Confirmation Dialog

struct DeleteConfirmationView: View {
    @State private var showConfirmation = false

    var body: some View {
        Button("Delete Account", role: .destructive) {
            showConfirmation = true
        }
        .confirmationDialog(
            "Delete Account",
            isPresented: $showConfirmation,
            titleVisibility: .visible
        ) {
            Button("Delete", role: .destructive) {
                deleteAccount()
            }
            Button("Cancel", role: .cancel) { }
        } message: {
            Text("This action cannot be undone.")
        }
    }
}

Loading and Progress

Progress Indicators

// Indeterminate spinner
ProgressView()
    .progressViewStyle(.circular)

// Determinate progress
ProgressView(value: downloadProgress, total: 1.0) {
    Text("Downloading...")
} currentValueLabel: {
    Text("\(Int(downloadProgress * 100))%")
}

// Custom loading view
struct LoadingOverlay: View {
    let message: String

    var body: some View {
        ZStack {
            Color.black.opacity(0.4)
                .ignoresSafeArea()

            VStack(spacing: 16) {
                ProgressView()
                    .scaleEffect(1.5)
                    .tint(.white)

                Text(message)
                    .font(.subheadline)
                    .foregroundStyle(.white)
            }
            .padding(24)
            .background(.ultraThinMaterial, in: RoundedRectangle(cornerRadius: 16))
        }
    }
}

Skeleton Loading

struct SkeletonRow: View {
    @State private var isAnimating = false

    var body: some View {
        HStack(spacing: 12) {
            Circle()
                .fill(.gray.opacity(0.3))
                .frame(width: 44, height: 44)

            VStack(alignment: .leading, spacing: 8) {
                RoundedRectangle(cornerRadius: 4)
                    .fill(.gray.opacity(0.3))
                    .frame(height: 14)
                    .frame(maxWidth: 200)

                RoundedRectangle(cornerRadius: 4)
                    .fill(.gray.opacity(0.2))
                    .frame(height: 12)
                    .frame(maxWidth: 150)
            }
        }
        .opacity(isAnimating ? 0.5 : 1.0)
        .animation(.easeInOut(duration: 0.8).repeatForever(), value: isAnimating)
        .onAppear { isAnimating = true }
    }
}

Async Content Loading

AsyncImage

AsyncImage(url: imageURL) { phase in
    switch phase {
    case .empty:
        ProgressView()
    case .success(let image):
        image
            .resizable()
            .aspectRatio(contentMode: .fill)
    case .failure:
        Image(systemName: "photo")
            .foregroundStyle(.secondary)
    @unknown default:
        EmptyView()
    }
}
.frame(width: 100, height: 100)
.clipShape(RoundedRectangle(cornerRadius: 8))

Task-Based Loading

struct AsyncContentView: View {
    @State private var items: [Item] = []
    @State private var isLoading = true
    @State private var error: Error?

    var body: some View {
        Group {
            if isLoading {
                ProgressView("Loading...")
            } else if let error {
                ContentUnavailableView(
                    "Failed to Load",
                    systemImage: "exclamationmark.triangle",
                    description: Text(error.localizedDescription)
                )
            } else if items.isEmpty {
                ContentUnavailableView(
                    "No Items",
                    systemImage: "tray",
                    description: Text("Add your first item to get started.")
                )
            } else {
                List(items) { item in
                    ItemRow(item: item)
                }
            }
        }
        .task {
            await loadItems()
        }
    }

    private func loadItems() async {
        do {
            items = try await api.fetchItems()
            isLoading = false
        } catch {
            self.error = error
            isLoading = false
        }
    }
}

Animations

Implicit Animations

struct AnimatedCard: View {
    @State private var isExpanded = false

    var body: some View {
        VStack {
            Text("Tap to expand")

            if isExpanded {
                Text("Additional content here")
                    .transition(.move(edge: .top).combined(with: .opacity))
            }
        }
        .padding()
        .frame(maxWidth: .infinity)
        .background(.blue.opacity(0.1), in: RoundedRectangle(cornerRadius: 12))
        .onTapGesture {
            withAnimation(.spring(response: 0.3, dampingFraction: 0.7)) {
                isExpanded.toggle()
            }
        }
    }
}

Custom Transitions

extension AnyTransition {
    static var slideAndFade: AnyTransition {
        .asymmetric(
            insertion: .move(edge: .trailing).combined(with: .opacity),
            removal: .move(edge: .leading).combined(with: .opacity)
        )
    }

    static var scaleAndFade: AnyTransition {
        .scale(scale: 0.8).combined(with: .opacity)
    }
}

Phase Animator (iOS 17+)

struct PulsingButton: View {
    var body: some View {
        Button("Tap Me") { }
            .buttonStyle(.borderedProminent)
            .phaseAnimator([false, true]) { content, phase in
                content
                    .scaleEffect(phase ? 1.05 : 1.0)
            } animation: { _ in
                .easeInOut(duration: 0.5)
            }
    }
}

Gestures

Drag Gesture

struct DraggableCard: View {
    @State private var offset = CGSize.zero
    @State private var isDragging = false

    var body: some View {
        RoundedRectangle(cornerRadius: 16)
            .fill(.blue)
            .frame(width: 200, height: 150)
            .offset(offset)
            .scaleEffect(isDragging ? 1.05 : 1.0)
            .gesture(
                DragGesture()
                    .onChanged { value in
                        offset = value.translation
                        isDragging = true
                    }
                    .onEnded { _ in
                        withAnimation(.spring()) {
                            offset = .zero
                            isDragging = false
                        }
                    }
            )
    }
}

Simultaneous Gestures

struct ZoomableImage: View {
    @State private var scale: CGFloat = 1.0
    @State private var lastScale: CGFloat = 1.0

    var body: some View {
        Image("photo")
            .resizable()
            .aspectRatio(contentMode: .fit)
            .scaleEffect(scale)
            .gesture(
                MagnificationGesture()
                    .onChanged { value in
                        scale = lastScale * value
                    }
                    .onEnded { _ in
                        lastScale = scale
                    }
            )
            .gesture(
                TapGesture(count: 2)
                    .onEnded {
                        withAnimation {
                            scale = 1.0
                            lastScale = 1.0
                        }
                    }
            )
    }
}

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.