Files
skills/ios-application-dev/references/swiftui-design-guidelines.md
shihao 6487becf60 Initial commit: add all skills files
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 16:52:49 +08:00

1168 lines
32 KiB
Markdown

# SwiftUI Design Guidelines
Design rules based on Apple Human Interface Guidelines for building native iOS interfaces with SwiftUI.
---
## Design Philosophy
iOS design prioritizes **content over chrome**. The interface should feel invisible—users focus on their tasks, not the UI.
**Key mindsets:**
1. **Let content breathe** — Use full-screen layouts, minimize borders and boxes, let images and text take center stage
2. **Leverage system conventions** — Users already know how iOS works; don't reinvent navigation, gestures, or controls
3. **Design for fingers** — Touch is imprecise; generous tap targets and forgiving gesture recognition matter more than pixel-perfect layouts
4. **Respect user choices** — Honor Dynamic Type, Dark Mode, Reduce Motion, and other accessibility settings as first-class requirements
**iOS 26+ Liquid Glass:**
The latest iOS introduces translucent UI elements that respond to lighting and content behind them. Typography is bolder, text tends left-aligned for easier scanning.
---
## 1. Layout & Safe Areas
**Impact:** CRITICAL
### 1.1 Minimum 44pt Touch Targets
All interactive elements must have minimum 44x44 **points** (not pixels—points scale with screen density).
```swift
Button(action: handleTap) {
Image(systemName: "heart.fill")
}
.frame(minWidth: 44, minHeight: 44)
```
Avoid placing critical interactions near screen edges where system gestures operate.
### 1.2 Respect Safe Areas
Never place interactive or essential content under the status bar, Dynamic Island, or home indicator. SwiftUI respects safe areas by default. Use `.ignoresSafeArea()` only for background fills, images, or decorative elements—never for text or interactive controls.
```swift
ZStack {
LinearGradient(colors: [.blue, .purple], startPoint: .top, endPoint: .bottom)
.ignoresSafeArea()
VStack {
Text("Welcome")
.font(.largeTitle)
Button("Get Started") { }
}
}
```
### 1.3 Primary Actions in Thumb Zone
Place primary actions at the bottom of the screen where the user's thumb naturally rests. Secondary actions and navigation belong at the top.
```swift
VStack {
ScrollView {
// Content
}
Spacer()
Button("Submit") { submit() }
.buttonStyle(.borderedProminent)
.padding(.horizontal)
.padding(.bottom)
}
```
### 1.4 Support All Screen Sizes
Design for iPhone SE (375pt) through iPad Pro (1024pt+). Use Size Classes to adapt:
```swift
@Environment(\.horizontalSizeClass) private var sizeClass
var body: some View {
if sizeClass == .compact {
VStack { content }
} else {
HStack { content }
}
}
```
| Size Class | Devices |
|------------|---------|
| Compact width | iPhone portrait, small iPhone landscape |
| Regular width | iPad, large iPhone landscape |
Use flexible layouts, avoid hardcoded widths:
```swift
HStack(spacing: 16) {
ForEach(categories) { category in
CategoryCard(category: category)
.frame(maxWidth: .infinity)
}
}
```
### 1.5 8pt Grid Alignment
Align spacing, padding, and element sizes to multiples of 8 points (8, 16, 24, 32, 40, 48). Use 4pt for fine adjustments.
### 1.6 Landscape Support
Support landscape orientation unless the app is task-specific (e.g., camera). Use `ViewThatFits` or `GeometryReader` for adaptive layouts.
```swift
ViewThatFits {
HStack { contentViews }
VStack { contentViews }
}
```
---
## 2. Navigation
**Impact:** CRITICAL
### 2.1 Tab Bar for Top-Level Sections
Use a tab bar at the bottom of the screen for 3 to 5 top-level sections. Each tab should represent a distinct category of content or functionality.
```swift
TabView(selection: $selectedTab) {
HomeView()
.tabItem {
Label("Home", systemImage: "house")
}
.tag(Tab.home)
DiscoverView()
.tabItem {
Label("Discover", systemImage: "magnifyingglass")
}
.tag(Tab.discover)
AccountView()
.tabItem {
Label("Account", systemImage: "person")
}
.tag(Tab.account)
}
```
### 2.2 Navigation Architecture
**Tab Bar (Flat)** — For 3-5 equal-importance sections
- Always visible except when covered by modals
- Each tab maintains its own navigation stack
- Most important content leftmost (easier thumb access)
**Hierarchical (Drill-Down)** — For tree-structured info
- Push/pop navigation with back button
- Minimize depth (3-4 levels max)
- Provide search as escape hatch for deep trees
**Modal (Focused Tasks)** — For self-contained workflows
- Full-screen for critical tasks
- Page sheet for dismissible tasks (swipe-down)
- Clear Done/Cancel with confirmation if data loss possible
Never use hamburger menus—they reduce feature discoverability significantly.
### 2.3 Large Titles in Primary Views
Use `.navigationBarTitleDisplayMode(.large)` for top-level views. Titles transition to inline when the user scrolls.
```swift
NavigationStack {
List(conversations) { conversation in
ConversationRow(conversation: conversation)
}
.navigationTitle("Inbox")
.navigationBarTitleDisplayMode(.large)
}
```
### 2.4 Never Override Back Swipe
The swipe-from-left-edge gesture for back navigation is a system-level expectation. Never attach custom gesture recognizers that interfere with it.
### 2.5 Use NavigationStack for Hierarchical Content
Use `NavigationStack` (not the deprecated `NavigationView`) for drill-down content. Use `NavigationPath` for programmatic navigation.
```swift
@State private var navPath = NavigationPath()
NavigationStack(path: $navPath) {
List(products) { product in
NavigationLink(value: product) {
ProductRow(product: product)
}
}
.navigationDestination(for: Product.self) { product in
ProductDetailView(product: product)
}
}
```
### 2.6 Preserve State Across Navigation
When users navigate back and then forward, or switch tabs, restore the previous scroll position and input state.
```swift
@SceneStorage("selectedTab") private var selectedTab = Tab.home
@SceneStorage("scrollPosition") private var scrollPosition: String?
```
---
## 3. Typography & Dynamic Type
**Impact:** HIGH
### 3.1 Use Built-in Text Styles
Always use semantic text styles—they scale with Dynamic Type automatically:
| Style | Usage |
|-------|-------|
| `.largeTitle` | Screen titles |
| `.title`, `.title2`, `.title3` | Section headers |
| `.headline` | Emphasized body text |
| `.body` | Primary content (17pt default) |
| `.callout` | Secondary emphasized |
| `.subheadline` | Supporting labels |
| `.footnote`, `.caption` | Tertiary info |
| `.caption2` | Minimum size (11pt) |
```swift
VStack(alignment: .leading, spacing: 8) {
Text("Article Title")
.font(.headline)
Text("Published by Author Name")
.font(.subheadline)
.foregroundStyle(.secondary)
Text(articleBody)
.font(.body)
}
```
### 3.2 Support Dynamic Type Including Accessibility Sizes
Dynamic Type can scale text up to approximately 200% at the largest accessibility sizes. Layouts must reflow—never truncate or clip essential text.
```swift
@Environment(\.dynamicTypeSize) private var typeSize
var body: some View {
if typeSize.isAccessibilitySize {
VStack(alignment: .leading) { content }
} else {
HStack { content }
}
}
```
### 3.3 Custom Fonts Must Scale
If you use a custom typeface, scale it with `Font.custom(_:size:relativeTo:)` so it responds to Dynamic Type.
```swift
Text("Brand Text")
.font(.custom("Avenir-Medium", size: 17, relativeTo: .body))
```
### 3.4 SF Pro as System Font
Use the system font (SF Pro) unless brand requirements dictate otherwise. SF Pro is optimized for legibility on Apple displays.
### 3.5 Minimum 11pt Text
Never display text smaller than 11pt. Prefer 17pt for body text. Use the `caption2` style (11pt) as the absolute minimum.
### 3.6 Hierarchy Through Weight and Size
Establish visual hierarchy through font weight and size. Do not rely solely on color to differentiate text levels.
### 3.7 SF Symbols
Use SF Symbols (6,900+ icons) instead of custom image assets:
```swift
// Basic usage with automatic text alignment
Label("Favorites", systemImage: "star.fill")
// Rendering modes
Image(systemName: "cloud.sun.rain")
.symbolRenderingMode(.hierarchical) // or .multicolor, .palette
.imageScale(.large) // .small, .medium, .large
```
SF Symbols automatically match text weight, scale with Dynamic Type, and align to text baselines. Let them size naturally—don't force them into fixed-dimension containers.
---
## 4. Color & Dark Mode
**Impact:** HIGH
### 4.1 Use Semantic System Colors
Never use hard-coded RGB, hex, or `.black`/`.white` directly. Use semantic colors:
**Labels:**
- `.primary`, `.secondary`, `.tertiary`, `.quaternary`
**Backgrounds:**
- `Color(.systemBackground)` — primary surface
- `Color(.secondarySystemBackground)` — cards, grouped
- `Color(.tertiarySystemBackground)` — nested elements
**System Colors (adapt to appearance):**
- `.blue`, `.red`, `.green`, `.orange`, `.yellow`, `.purple`, `.pink`, `.cyan`, `.mint`, `.teal`, `.indigo`, `.brown`, `.gray`
```swift
VStack {
Text("Primary content")
.foregroundStyle(.primary)
Text("Supporting info")
.foregroundStyle(.secondary)
}
.background(Color(.systemBackground))
```
### 4.2 Custom Colors Need 4 Variants
For custom colors, define in asset catalog with all appearance combinations:
1. Light mode
2. Dark mode
3. Light mode + High Contrast
4. Dark mode + High Contrast
```swift
Text("Branded element")
.foregroundStyle(Color("AccentBrand"))
```
For dynamic colors in code:
```swift
let dynamicColor = UIColor { traits in
traits.userInterfaceStyle == .dark
? UIColor(red: 0.9, green: 0.9, blue: 1.0, alpha: 1.0)
: UIColor(red: 0.1, green: 0.1, blue: 0.2, alpha: 1.0)
}
```
### 4.3 Never Rely on Color Alone
Always pair color with text, icons, or shapes to convey meaning. Approximately 8% of men have some form of color vision deficiency.
```swift
HStack(spacing: 6) {
Image(systemName: "exclamationmark.triangle.fill")
Text("Connection failed")
}
.foregroundStyle(.red)
```
### 4.4 4.5:1 Contrast Ratio Minimum
All text must meet WCAG AA contrast ratios: 4.5:1 for normal text, 3:1 for large text (18pt+ or 14pt+ bold).
### 4.5 Support Display P3 Wide Gamut
Use Display P3 color space for vibrant, accurate colors on modern iPhones. Define colors in the asset catalog with the Display P3 gamut.
### 4.6 Background Hierarchy
Layer backgrounds to create visual depth:
```swift
// Level 1: Main view background
Color(.systemBackground)
// Level 2: Cards, grouped sections
Color(.secondarySystemBackground)
// Level 3: Nested elements within cards
Color(.tertiarySystemBackground)
```
### 4.7 One Accent Color for Interactive Elements
Choose a single tint/accent color for all interactive elements (buttons, links, toggles). This creates a consistent, learnable visual language.
```swift
@main
struct MyApp: App {
var body: some Scene {
WindowGroup {
ContentView()
.tint(.orange)
}
}
}
```
---
## 5. Accessibility
**Impact:** CRITICAL
### 5.1 VoiceOver Labels on All Interactive Elements
Every button, control, and interactive element must have a meaningful accessibility label.
```swift
Button(action: toggleFavorite) {
Image(systemName: isFavorite ? "heart.fill" : "heart")
}
.accessibilityLabel(isFavorite ? "Remove from favorites" : "Add to favorites")
```
### 5.2 Logical VoiceOver Navigation Order
Ensure VoiceOver reads elements in a logical order. Use `.accessibilitySortPriority()` to adjust when the visual layout doesn't match the reading order.
```swift
HStack {
Text("$49.99")
.accessibilitySortPriority(2)
Text("Premium Plan")
.accessibilitySortPriority(1)
}
```
### 5.3 Support Bold Text
When the user enables Bold Text in Settings, SwiftUI text styles handle this automatically. Custom text must respond to `UIAccessibility.isBoldTextEnabled`.
### 5.4 Support Reduce Motion
Disable decorative animations and parallax when Reduce Motion is enabled.
```swift
@Environment(\.accessibilityReduceMotion) private var reduceMotion
var body: some View {
CardView()
.animation(reduceMotion ? nil : .spring(duration: 0.4), value: expanded)
}
```
### 5.5 Support Increase Contrast
When the user enables Increase Contrast, ensure custom colors have higher-contrast variants. Use `@Environment(\.colorSchemeContrast)` to detect.
### 5.6 Don't Convey Info Only by Color, Shape, or Position
Information must be available through multiple channels. Pair visual indicators with text or accessibility descriptions.
### 5.7 Alternative Interactions for All Gestures
Every custom gesture must have an equivalent tap-based or menu-based alternative for users who cannot perform complex gestures.
### 5.8 Support Switch Control and Full Keyboard Access
Ensure all interactions work with Switch Control (external switches) and Full Keyboard Access (Bluetooth keyboards). Test navigation order and focus behavior.
---
## 6. Gestures & Input
**Impact:** HIGH
### 6.1 Use Standard Gestures
Stick to gestures users already know:
- **Tap** — Select items, trigger buttons
- **Long press** — Show context menus, enter edit mode
- **Horizontal swipe** — List row actions (delete/archive), back navigation
- **Vertical swipe** — Scroll content, dismiss sheets
- **Pinch** — Scale images/maps
- **Rotate** — Adjust angle (photos, maps)
### 6.2 Never Override System Gestures
iOS reserves these edge gestures—do not intercept:
- Left edge swipe → back navigation
- Top-left pull → Notification Center
- Top-right pull → Control Center
- Bottom edge swipe → home/app switcher
### 6.3 Custom Gestures Must Be Discoverable
If you add a custom gesture, provide visual hints (e.g., a grabber handle) and ensure the action is also available through a visible button or menu item.
### 6.4 Support All Input Methods
Design for touch first, but also support hardware keyboards, assistive devices (Switch Control, head tracking), and pointer input.
---
## 7. Components
**Impact:** HIGH
### 7.1 Button Styles
Use the built-in button styles appropriately:
```swift
VStack(spacing: 16) {
Button("Checkout") { checkout() }
.buttonStyle(.borderedProminent)
Button("Add to Wishlist") { addToWishlist() }
.buttonStyle(.bordered)
Button("Remove Item", role: .destructive) { removeItem() }
}
```
### 7.2 Alerts — Critical Info Only
Use alerts sparingly for critical information that requires a decision. Prefer 2 buttons; maximum 3.
```swift
.alert("Discard Draft?", isPresented: $showDiscardAlert) {
Button("Discard", role: .destructive) { discardDraft() }
Button("Keep Editing", role: .cancel) { }
} message: {
Text("Your unsaved changes will be lost.")
}
```
### 7.3 Sheets for Scoped Tasks
Present sheets for self-contained tasks. Always provide a way to dismiss (close button or swipe down).
```swift
.sheet(isPresented: $showEditor) {
NavigationStack {
EditorView()
.navigationTitle("Edit Profile")
.toolbar {
ToolbarItem(placement: .cancellationAction) {
Button("Cancel") { showEditor = false }
}
ToolbarItem(placement: .confirmationAction) {
Button("Save") { saveProfile() }
}
}
}
.presentationDetents([.medium, .large])
.presentationDragIndicator(.visible)
}
```
### 7.4 Lists — The Foundation of iOS Apps
Most iOS apps are lists ("90% of mobile design is list design").
**List Styles:**
- `.insetGrouped` — Modern default (rounded corners, margins)
- `.grouped` — Traditional grouped sections
- `.plain` — Edge-to-edge rows
- `.sidebar` — Three-column iPad layout
**Swipe Actions:**
- Leading swipe → Positive actions (mark read, archive)
- Trailing swipe → Destructive actions (delete at far right)
- Maximum 3-4 actions per side
**Row Accessories:**
- Chevron → Indicates navigation
- Checkmark → Shows selection
- Detail button → Additional info without navigation
```swift
List {
Section("Notifications") {
ForEach(notifications) { notification in
NotificationRow(notification: notification)
.swipeActions(edge: .trailing, allowsFullSwipe: true) {
Button(role: .destructive) {
delete(notification)
} label: {
Label("Delete", systemImage: "trash")
}
Button {
markRead(notification)
} label: {
Label("Read", systemImage: "envelope.open")
}
.tint(.blue)
}
.swipeActions(edge: .leading) {
Button {
pin(notification)
} label: {
Label("Pin", systemImage: "pin")
}
.tint(.orange)
}
}
}
}
.listStyle(.insetGrouped)
```
### 7.5 Tab Bar Behavior
- Use SF Symbols for tab icons — filled variant for the selected tab, outline for unselected
- Never hide the tab bar when navigating deeper within a tab
- Badge important counts with `.badge()`
```swift
NotificationsView()
.tabItem {
Label("Notifications", systemImage: "bell")
}
.badge(unreadCount)
```
### 7.6 Search
Place search using `.searchable()`. Provide search suggestions and support recent searches.
```swift
NavigationStack {
List(searchResults) { item in
ItemRow(item: item)
}
.searchable(text: $query, prompt: "Search products")
.searchSuggestions {
ForEach(recentSearches, id: \.self) { term in
Text(term)
.searchCompletion(term)
}
}
}
```
### 7.7 Context Menus
Use context menus (long press) for secondary actions. Never use a context menu as the only way to access an action.
```swift
ImageThumbnail(image: image)
.contextMenu {
Button { shareImage(image) } label: {
Label("Share", systemImage: "square.and.arrow.up")
}
Button { copyImage(image) } label: {
Label("Copy", systemImage: "doc.on.doc")
}
Divider()
Button(role: .destructive) { deleteImage(image) } label: {
Label("Delete", systemImage: "trash")
}
}
```
### 7.8 Forms and Input
**Text Fields:**
- 44pt minimum height
- Match keyboard type to input (`.emailAddress`, `.numberPad`, `.URL`)
- Clear button when text entered
- Placeholder uses `.quaternary` label color
```swift
Form {
Section("Account") {
TextField("Email", text: $email)
.textContentType(.emailAddress)
.keyboardType(.emailAddress)
.autocapitalization(.none)
SecureField("Password", text: $password)
.textContentType(.password)
}
Section {
Button("Sign In") { signIn() }
.disabled(email.isEmpty || password.isEmpty)
}
}
```
**Pickers:**
- Inline → 3-7 options
- Menu → 2-5 options (iOS 14+)
- Wheel → Date/time or long lists
### 7.9 Progress Indicators
- Determinate `ProgressView(value:total:)` for operations with known duration
- Indeterminate `ProgressView()` for unknown duration
- Never block the entire screen with a spinner
```swift
VStack {
ProgressView(value: uploadProgress, total: 1.0)
.progressViewStyle(.linear)
Text("\(Int(uploadProgress * 100))% uploaded")
.font(.caption)
.foregroundStyle(.secondary)
}
```
---
## 8. Patterns
**Impact:** MEDIUM
### 8.1 Onboarding — Max 3 Pages, Skippable
Keep onboarding to 3 or fewer pages. Always provide a skip option. Defer sign-in until the user needs authenticated features.
```swift
TabView(selection: $currentPage) {
OnboardingPage(icon: "sparkles", title: "Smart Features", description: "...")
.tag(0)
OnboardingPage(icon: "bell.badge", title: "Stay Notified", description: "...")
.tag(1)
OnboardingPage(icon: "lock.shield", title: "Private & Secure", description: "...")
.tag(2)
}
.tabViewStyle(.page)
.overlay(alignment: .topTrailing) {
Button("Skip") { finishOnboarding() }
.padding()
}
```
### 8.2 Loading — Skeleton Views, No Blocking Spinners
Use skeleton/placeholder views that match the layout of the content being loaded. Never show a full-screen blocking spinner.
```swift
if isLoading {
ForEach(0..<5, id: \.self) { _ in
ArticleRowPlaceholder()
.redacted(reason: .placeholder)
}
} else {
ForEach(articles) { article in
ArticleRow(article: article)
}
}
```
### 8.3 Launch Screen — Match First Screen
The launch storyboard must visually match the initial screen of the app. No splash logos, no branding screens. This creates the perception of instant launch.
### 8.4 Modality — Use Sparingly
Present modal views only when the user must complete or abandon a focused task. Always provide a clear dismiss action. Never stack modals on top of modals.
### 8.5 Notifications — High Value Only
Only send notifications for content the user genuinely cares about. Support actionable notifications. Categorize notifications so users can control them granularly.
### 8.6 Settings Placement
- Frequent settings: In-app settings screen accessible from a profile or gear icon
- Privacy/permission settings: Defer to the system Settings app via URL scheme
- Never duplicate system-level controls in-app
### 8.7 Action Sheets
For destructive or multiple-choice actions:
```swift
.confirmationDialog("Delete Photo?", isPresented: $showDelete, titleVisibility: .visible) {
Button("Delete", role: .destructive) { deletePhoto() }
Button("Cancel", role: .cancel) { }
} message: {
Text("This action cannot be undone.")
}
```
- Destructive action at top (red)
- Cancel at bottom
- Dismiss by tapping outside
### 8.8 Pull-to-Refresh
Standard pattern for content updates:
```swift
List(items) { item in
ItemRow(item: item)
}
.refreshable {
await loadNewItems()
}
```
### 8.9 Haptic Feedback
Provide tactile response for significant actions:
| Generator | Usage |
|-----------|-------|
| `UIImpactFeedbackGenerator` | Physical impacts (.light, .medium, .heavy) |
| `UINotificationFeedbackGenerator` | Success, warning, error |
| `UISelectionFeedbackGenerator` | Selection changes |
```swift
Button("Complete") {
let feedback = UINotificationFeedbackGenerator()
feedback.notificationOccurred(.success)
markComplete()
}
```
---
## 9. Privacy & Permissions
**Impact:** HIGH
### 9.1 Request Permissions in Context
Request a permission at the moment the user takes an action that needs it—never at app launch.
```swift
Button("Take Photo") {
AVCaptureDevice.requestAccess(for: .video) { granted in
if granted {
showCamera = true
}
}
}
```
### 9.2 Explain Before System Prompt
Show a custom explanation screen before triggering the system permission dialog. The system dialog only appears once—if the user denies, the app must direct them to Settings.
```swift
struct LocationPermissionView: View {
var body: some View {
VStack(spacing: 20) {
Image(systemName: "location.fill")
.font(.system(size: 48))
.foregroundStyle(.blue)
Text("Find Nearby Places")
.font(.title2.bold())
Text("We use your location to show relevant results. Your location is never stored or shared.")
.multilineTextAlignment(.center)
.foregroundStyle(.secondary)
Button("Enable Location") {
locationManager.requestWhenInUseAuthorization()
}
.buttonStyle(.borderedProminent)
Button("Not Now") { dismiss() }
.foregroundStyle(.secondary)
}
.padding()
}
}
```
### 9.3 Support Sign in with Apple
If the app offers any third-party sign-in (Google, Facebook), it must also offer Sign in with Apple. Present it as the first option.
### 9.4 Don't Require Accounts Unless Necessary
Let users explore the app before requiring sign-in. Gate only features that genuinely need authentication (purchases, sync, social features).
### 9.5 App Tracking Transparency
If you track users across apps or websites, display the ATT prompt. Respect denial—do not degrade the experience for users who opt out.
### 9.6 Location Button for One-Time Access
Use `LocationButton` for actions that need location once without requesting ongoing permission.
```swift
LocationButton(.currentLocation) {
fetchNearbyResults()
}
.symbolVariant(.fill)
.labelStyle(.titleAndIcon)
```
---
## 10. System Integration
**Impact:** MEDIUM
### 10.1 Widgets for Glanceable Data
Provide widgets using WidgetKit for information users check frequently. Widgets are not interactive (beyond tapping to open the app), so show the most useful snapshot.
### 10.2 App Shortcuts for Key Actions
Define App Shortcuts so users can trigger key actions from Siri, Spotlight, and the Shortcuts app.
```swift
struct MyAppShortcuts: AppShortcutsProvider {
static var appShortcuts: [AppShortcut] {
AppShortcut(
intent: QuickAddIntent(),
phrases: ["Add item in \(.applicationName)"],
shortTitle: "Quick Add",
systemImageName: "plus.circle"
)
}
}
```
### 10.3 Spotlight Indexing
Index app content with `CSSearchableItem` so users can find it from Spotlight search.
### 10.4 Share Sheet Integration
Support the system share sheet for content that users might want to send elsewhere.
```swift
ShareLink(item: article.url, subject: Text(article.title)) {
Label("Share Article", systemImage: "square.and.arrow.up")
}
```
### 10.5 Live Activities
Use Live Activities and the Dynamic Island for real-time, time-bound events (delivery tracking, sports scores, workouts).
### 10.6 Handle Interruptions Gracefully
Save state and pause gracefully when interrupted by phone calls, Siri invocations, notifications, app switcher, or FaceTime SharePlay.
```swift
@Environment(\.scenePhase) private var scenePhase
var body: some View {
ContentView()
.onChange(of: scenePhase) { _, newPhase in
switch newPhase {
case .active:
resumeActivity()
case .inactive:
pauseActivity()
case .background:
saveState()
@unknown default:
break
}
}
}
```
---
## Quick Reference
### Navigation & Structure
| Component | When to Use |
|-----------|-------------|
| `TabView` | 3-5 main app sections |
| `NavigationStack` | Hierarchical content drill-down |
| `.sheet` | Focused tasks requiring user completion |
| `.alert` | Decisions that block workflow |
| `.contextMenu` | Additional actions (always provide alternatives) |
### Data Display
| Component | When to Use |
|-----------|-------------|
| `List` | Scrollable rows with sections |
| `LazyVGrid` / `LazyHGrid` | Grid layouts |
| `.searchable` | Filterable content |
| `ProgressView` | Loading or task progress |
### User Input
| Component | When to Use |
|-----------|-------------|
| `TextField` | Single-line text |
| `TextEditor` | Multi-line text |
| `Picker` | Selection from options |
| `Toggle` | Binary on/off choice |
| `Stepper` | Numeric increment/decrement |
### System Features
| Component | When to Use |
|-----------|-------------|
| `ShareLink` | Content sharing |
| `LocationButton` | One-time location access |
| `PhotosPicker` | Image selection |
| `UIImpactFeedbackGenerator` | Tactile response |
---
## Anti-Patterns
Avoid these common HIG violations:
| Pattern | Problem | Solution |
|---------|---------|----------|
| Hamburger/drawer menu | Hides navigation, users miss features | Use TabView with 3-5 tabs |
| Broken back swipe | Custom gestures block system navigation | Keep NavigationStack default behavior |
| Full-screen spinner | App feels frozen, no progress indication | Use skeleton views with `.redacted()` |
| Logo splash screen | Artificial delay, wastes user time | Match launch screen to first view |
| Permissions at launch | Users deny without context | Request when action requires it |
| Fixed font sizes | Breaks Dynamic Type, accessibility issues | Use `.font(.body)` semantic styles |
| Color-only status | Colorblind users miss information | Add icons or text labels |
| Alert overuse | Interrupts flow for minor info | Use inline messages or banners |
| Hidden tab bar | Users lose navigation context | Keep tab bar visible on push |
| Content in unsafe areas | Text hidden under notch/Dynamic Island | Only ignore safe area for backgrounds |
| No modal dismiss | Users trapped in view | Add cancel button and swipe dismiss |
| Gesture-only actions | Accessibility users blocked | Provide button/menu alternatives |
| Small tap targets | Frequent mis-taps | Minimum 44x44pt hit area |
| Nested modals | Navigation confusion | Use NavigationStack within single sheet |
| Hardcoded colors | Broken in Dark Mode | Use semantic colors or asset variants |
---
## Review Checklist
Code review checklist for SwiftUI apps:
### Layout
- [ ] Interactive elements have 44pt minimum touch area
- [ ] Essential content stays within safe area bounds
- [ ] Main actions positioned for one-handed use (bottom)
- [ ] UI works across iPhone SE to Pro Max screen sizes
- [ ] Spacing uses 8pt increments
### Navigation
- [ ] Main sections use bottom TabView (3-5 tabs)
- [ ] No drawer/hamburger navigation
- [ ] Root views show large navigation titles
- [ ] System back gesture not blocked
- [ ] Tab state persists when switching
### Text & Fonts
- [ ] Text uses semantic styles (`.body`, `.headline`, etc.)
- [ ] Dynamic Type works at all sizes including accessibility
- [ ] Content reflows without truncation at large sizes
- [ ] No text below 11pt
### Colors
- [ ] Uses `.primary`, `.secondary`, `Color(.systemBackground)`
- [ ] Custom colors have light/dark variants in assets
- [ ] Status indicators combine color with icon/text
- [ ] Text contrast ratio meets WCAG AA
### Accessibility
- [ ] Icon buttons have `.accessibilityLabel()`
- [ ] VoiceOver order matches logical flow
- [ ] Animations respect `accessibilityReduceMotion`
- [ ] All actions have non-gesture alternatives
### Modals & Alerts
- [ ] Alerts reserved for critical decisions only
- [ ] Sheets provide clear dismiss mechanism
- [ ] No stacked modal presentations
### Permissions
- [ ] Permissions requested at point of use
- [ ] Pre-permission explanation screens used
- [ ] Core features work without sign-in
---
## iPad Adaptation
iPad users expect different interaction patterns:
**Layout:** Use `NavigationSplitView` for master-detail:
```swift
NavigationSplitView(columnVisibility: $columnVisibility) {
SidebarView()
} content: {
ListContentView()
} detail: {
DetailView()
}
.navigationSplitViewStyle(.balanced)
```
**Presentation:** Action sheets become popovers automatically, but you can force popover:
```swift
.popover(isPresented: $showOptions) {
OptionsView()
}
```
**Keyboard:** Add shortcuts for power users:
```swift
.keyboardShortcut("n", modifiers: .command) // Cmd+N
```
**Drag & Drop:** Enable cross-app data transfer:
```swift
.draggable(item)
.dropDestination(for: Item.self) { items, location in
handleDrop(items)
return true
}
```
---
## Pre-Release Verification
Run through these scenarios before shipping:
**Visual consistency:**
- Switch between Light/Dark mode—does everything remain readable?
- Crank Dynamic Type to maximum—does layout adapt or break?
- Enable Bold Text—do custom fonts respond?
**Interaction quality:**
- Can you complete every action using only VoiceOver?
- Do all buttons feel tappable on first try (no mis-taps)?
- Does back-swipe work everywhere in navigation?
**Edge cases:**
- What happens on iPhone SE's small screen?
- What happens on iPad with keyboard attached?
- What shows when network fails mid-operation?
- What happens if user denies permissions?
**Platform compliance:**
- Are you using SF Symbols instead of custom icon PNGs?
- Are all colors from semantic palette or asset catalog with variants?
- Do destructive actions require explicit confirmation?
---
*SwiftUI, SF Symbols, Dynamic Island, and Apple are trademarks of Apple Inc.*