ios-hig
Apple Human Interface Guidelines for iOS and iPadOS. Load for iOS or iPadOS design tasks.
SKILL.mdskills/mobile/ios-hig/SKILL.md
GitHubDetection Signals
Load this skill when any of the following are present:
| Signal | Location |
|---|---|
| iOS target in cross-platform project | React Native, Flutter, or pubspec.yaml with iOS support |
| Native iOS/iPadOS project | .xcodeproj, .xcworkspace, Info.plist, Swift source files |
| SwiftUI or UIKit usage | import SwiftUI, import UIKit in source files |
| Design task for iOS | User mentions "iOS design", "HIG", "Apple guidelines", or "SwiftUI design" |
Core Principle — Feel Native
iOS users expect apps to feel like a natural extension of the operating system. Deviation from platform patterns — even aesthetically — creates friction. When in doubt, prefer the native pattern over a custom one.
Navigation Patterns
Choose the navigation model that matches the information architecture. Never mix models without a clear reason.
Tab Bar
- Use for 3–5 top-level destinations that are peers (no hierarchy between them)
- Tabs are always visible and always accessible — never hide or disable individual tabs
- Icons: use SF Symbols; active tab uses filled variant +
tintcolor; inactive uses outlined variant +secondaryLabelcolor - Tab bar height: 49pt (83pt with home indicator safe area on notched devices)
- Labels: short (1–2 words max); truncate with
…only as a last resort
Navigation Stack (Push)
- Use for hierarchical content — each level drills deeper into detail
- Back button appears automatically; never replace it with a custom "Back" label unless the destination title is ambiguous
- Navigation bar title: use
.largestyle at the root level,.inlinefor deeper levels - Never push more than 3–4 levels without a shortcut back to root (e.g., long-press back button)
Modal Presentation
| Style | Usage |
|---|---|
Sheet (.sheet) |
Lightweight tasks that don't require full context switch |
Full-screen cover (.fullScreenCover) |
Immersive tasks (camera, onboarding, video); user cannot swipe to dismiss |
| Popover (iPad) | Contextual information anchored to a control; auto-adapts to sheet on iPhone |
- Sheets support pull-to-dismiss by default — preserve this behavior; only disable when unsaved data is at risk (show a confirmation action sheet instead)
- Never present a sheet from within a sheet — use navigation push instead
- Full-screen covers must always provide an explicit dismiss control (button or gesture)
Split View (iPad)
- Use
NavigationSplitViewfor two- or three-column layouts on iPad - Sidebar (first column): persistent navigation; list of sections or categories
- Content (second column): list or index within a section
- Detail (third column): full detail view
- Collapse gracefully to a single-column navigation stack on iPhone and compact-width iPad
Controls
SF Symbols
- Always prefer SF Symbols over custom icons — they scale with Dynamic Type, support weight variants, and adapt to Dark Mode automatically
- Match symbol weight to the surrounding text weight:
regulartext →regularsymbol - Use filled variants for selected/active states; outlined for inactive
- Symbols used as interactive controls must have an accessibility label
- Custom symbols must follow the SF Symbols grid and optical alignment — use the SF Symbols app to verify
Buttons
| Style | Usage |
|---|---|
Filled (.borderedProminent) |
Primary action; one per screen |
Tinted (.bordered with tint) |
Secondary action; safe to use multiple times |
Gray (.bordered) |
Tertiary or neutral actions |
Plain (.plain) |
Low-emphasis, inline, or destructive actions |
- Minimum touch target: 44×44pt — use
.contentShape(Rectangle())to expand hit area without changing visual size - Destructive actions must use
.destructiverole — renders in red and triggers a confirmation if needed - Button labels: verb phrases ("Save Changes", "Delete Account") — never noun-only labels ("OK")
Toggles and Pickers
Toggle(Switch): for immediate binary settings — changes apply instantly without a "Save" buttonPicker: use.segmentedstyle for 2–4 mutually exclusive short options; use.menustyle for longer listsStepper: for incrementing/decrementing numeric values within a known rangeSlider: for continuous values where precision is less critical than range explorationDatePicker: always use the system date picker — never build a custom one
Text Input
TextField: single-line; setkeyboardType,textContentType, andautocorrectionDisabledappropriatelySecureField: for passwords — never useTextFieldfor passwordsTextEditor: multi-line; add a character count label when there is a limit- Always dismiss the keyboard when the user taps outside the field (
onTapGestureon the background) - Return key label should match the action: "Done", "Search", "Send", "Next"
Typography — SF Pro
Apple's system font family is SF Pro (iOS/macOS) and SF Pro Rounded (friendly contexts). Use it via Dynamic Type — never specify a fixed font size.
Dynamic Type Scales
| Style | Default Size | Usage |
|---|---|---|
largeTitle |
34pt | Hero page titles (used sparingly) |
title1 |
28pt | Primary screen headings |
title2 |
22pt | Secondary headings |
title3 |
20pt | Tertiary headings |
headline |
17pt semibold | List row primaries, emphasized labels |
body |
17pt | Primary body text |
callout |
16pt | Secondary body, sidebars |
subheadline |
15pt | Supporting labels |
footnote |
13pt | Secondary descriptions, timestamps |
caption1 |
12pt | Image captions, form helper text |
caption2 |
11pt | Smallest visible labels |
Rules:
- Always use
.font(.body),.font(.headline), etc. — neverFont.system(size: 17) - Support Accessibility text sizes (up to ~310% of default) — test with "Larger Text" in Accessibility settings
- When Accessibility sizes are enabled, consider moving labels to a vertical stack using
@ScaledMetricorViewThatFits - Minimum:
caption2(11pt) — never use sizes below this
Layout
Safe Area
- Never place interactive content or critical text outside the safe area — it will be obscured by notch, Dynamic Island, or home indicator
- Use
.safeAreaInset()(SwiftUI) orsafeAreaLayoutGuide(UIKit) — never hard-code pixel offsets - Bottom content (buttons, tab bars): account for the home indicator (
34pton notched devices)
Spacing and Margins
- Standard content margin: 16pt from screen edges (20pt on Plus/Max models)
- Group related content with consistent spacing; use the 8pt grid as the base unit
- List rows: minimum 44pt height for tappable rows
- Section headers:
listRowInsetspreserves the standard left inset unless deliberately removed
Grids and Collections
- Use
LazyVGrid/LazyHGrid(SwiftUI) orUICollectionViewCompositionalLayout(UIKit) for grid layouts — never manual frame placement - Item spacing: 8pt default; increase to 16pt for card-style items
- Always account for variable row heights — never assume fixed item height in a collection
Dark Mode
iOS automatically switches between light and dark appearances. Designs must support both.
Color Usage
- Always use semantic system colors — they adapt automatically:
.primary,.secondary,.background,.secondaryBackground,.grouped Background,.separator,.label,.secondaryLabel - Custom colors must define both light and dark variants in the Asset Catalog (Appearances: Any, Dark)
- Never hard-code
Color.whitefor backgrounds orColor.blackfor text — use semantic equivalents
Images and Icons
- SF Symbols adapt automatically — no extra work needed
- Custom images: provide light and dark variants in the Asset Catalog when they differ
- Avoid images with embedded text — they do not adapt to Dark Mode
Elevation and Depth
- iOS does not use colored tonal elevation like Material — use shadows, blur (
.background(.ultraThinMaterial)), or grouping to separate layers - Materials (
ultraThinMaterial,thinMaterial,regularMaterial,thickMaterial,ultraThickMaterial) adapt to both appearances automatically
Accessibility
VoiceOver
- Every interactive element must have an
accessibilityLabel(what it is) and, when needed, anaccessibilityHint(what happens when activated) - Decorative images: mark as
.accessibilityHidden(true) - Custom controls: implement
accessibilityValuefor current state andaccessibilityAdjustableActionfor sliders/pickers - Test all key flows with VoiceOver enabled before release
Reduce Motion
- Respect the Reduce Motion accessibility setting — replace all non-essential animations with simple crossfades or instant transitions
- In SwiftUI:
@Environment(\.accessibilityReduceMotion) var reduceMotion - Never use motion to convey meaning that cannot be understood without it
Increase Contrast
- Respect the Increase Contrast setting — borders and separators become more prominent automatically with system colors; custom colors must provide high-contrast variants
- In SwiftUI:
@Environment(\.accessibilityDifferentiateWithoutColor)and@Environment(\.colorSchemeContrast)
Touch Targets
- Minimum: 44×44pt for all tappable elements
- For small visual elements (icons, toggles), expand the hit area with padding or
.contentShape(Rectangle())
iPhone vs iPad Adaptations
The same app must adapt its layout based on the available space — not serve a different design.
| Aspect | iPhone (Compact) | iPad (Regular) |
|---|---|---|
| Navigation | Tab Bar + Navigation Stack | Split View or Tab Bar |
| Modals | Full-width sheets | Popovers, centered sheets |
| Content columns | Single column | Two or three columns |
| Keyboard | Full-screen, pushes content | Floating or docked; content shifts |
| Pointer (iPadOS) | N/A | Support hover effects (.hoverEffect()) |
- Use
@Environment(\.horizontalSizeClass)to branch layout —compact= iPhone / iPad portrait in some cases;regular= iPad landscape and most iPad orientations - Never detect device model (
UIDevice.current.userInterfaceIdiom) to branch layout — use size class; it handles Split View and Stage Manager correctly - Test on iPad with Stage Manager enabled — the app may appear in various window sizes
Platform Conventions to Never Break
These behaviors are deeply ingrained in iOS users. Deviating causes immediate friction:
- Pull-to-refresh: always supported in scrollable lists that show server data
- Swipe-to-delete: always supported in editable lists (
onDeletein SwiftUI) - Long-press context menu: offer contextual actions on tappable items (
.contextMenu) - Back swipe: never disable edge-swipe-to-go-back; it is a core navigation gesture
- Scroll-to-top: tapping the status bar scrolls the active scroll view to the top — do not break this
- Share sheet: use
UIActivityViewController/ShareLink— never build a custom share UI - System alerts: use
UIAlertController/.alert()for OS-level confirmations — never custom modal dialogs for destructive actions