
Ui Review
- 3 installs
- 591 repo stars
- Updated July 24, 2026
- rshankras/claude-code-apple-skills
Reviews SwiftUI code for iOS/watchOS Human Interface Guidelines compliance, font usage, Dynamic Type support, and accessibility.
About
Performs a UI/UX review of SwiftUI code against Apple's Human Interface Guidelines, font best practices, Dynamic Type, and accessibility standards. A developer uses it to verify iOS/watchOS interface design against Apple standards.
- Checks HIG compliance, fonts, and Dynamic Type
- Accessibility audit for iOS and watchOS
Ui Review by the numbers
- 3 all-time installs (skills.sh)
- +1 installs in the week ending Aug 5, 2026 (Skillselion tracking)
- Ranked #1,550 of 1,880 Design & UI/UX skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/rshankras/claude-code-apple-skills --skill ui-reviewAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 3 |
|---|---|
| repo stars | ★ 591 |
| Last updated | July 24, 2026 |
| Repository | rshankras/claude-code-apple-skills ↗ |
What it does
Reviews SwiftUI code for iOS/watchOS Human Interface Guidelines compliance, font usage, Dynamic Type support, and accessibility.
Files
UI Review Skill
Performs comprehensive UI/UX review of SwiftUI code against Apple's Human Interface Guidelines, font best practices, and accessibility standards for iOS and watchOS.
When This Skill Activates
Use this skill when the user:
- Asks to review UI/UX code
- Mentions HIG compliance or Apple guidelines
- Requests accessibility audit
- Wants font usage checked
- Asks about Dynamic Type support
- Requests design review against Apple standards
Review Process
1. Identify Files to Review
- If user specifies files/views, review those
- Otherwise, ask which views to review or scan recent SwiftUI files
- Prioritize user-facing views over components
2. Load Reference Materials
Before starting the review, familiarize yourself with the reference materials by reading the following files in .claude/skills/ui-review/:
- hig-checklist.md - Comprehensive HIG compliance checklist for iOS and watchOS
- font-guidelines.md - Font usage, Dynamic Type, and typography best practices
- accessibility-quick-ref.md - Quick reference for accessibility implementation
You may also reference the official Apple guidelines using WebFetch when needed:
- iOS HIG: https://developer.apple.com/design/human-interface-guidelines/designing-for-ios
- watchOS HIG: https://developer.apple.com/design/human-interface-guidelines/designing-for-watchos
3. Review Categories
Apply these review categories based on the code type:
HIG Compliance:
- Layout & spacing (tap targets, safe areas, padding)
- Navigation patterns (NavigationStack, sheets, alerts)
- Colors & visuals (semantic colors, dark mode, contrast)
- Platform-specific requirements (iOS vs watchOS)
- Loading/empty/error states
Font Usage:
- Dynamic Type support
- System text styles vs fixed sizes
- Font hierarchy and semantic usage
- Custom fonts scaling properly
- Text formatting and truncation
Accessibility:
- Labels and hints for interactive elements
- Traits and roles
- VoiceOver navigation order
- Custom actions
- Dynamic content announcements
- Testing with assistive technologies
4. Common Issues to Flag
Anti-patterns:
- Hardcoded colors (
.foregroundColor(.black)) - Fixed font sizes (
.font(.system(size: 14))) - Missing accessibility labels on icon-only buttons
- Tap targets smaller than 44pt (iOS) or 40pt (watchOS)
- Important info conveyed by color only
- Missing loading/error states
- Direct UIColor usage (use
Color(.systemBackground)) .frame()without considering Dynamic Type expansion- Missing keyboard shortcuts (iPad/Mac)
Good Patterns:
- Semantic color usage
- System font styles with Dynamic Type
- Comprehensive accessibility labels
- Clear visual hierarchy
- Consistent spacing
- Proper error handling
- Responsive layouts
Output Format
Provide review in this structure:
✅ HIG Compliance
- List items that comply well
- Highlight good practices
⚠️ HIG Issues Found
- Specific line references:
filename.swift:lineNumber - Description of issue
- Suggested fix with code example
✅ Font Usage
- Proper Dynamic Type usage
- Good font hierarchy
⚠️ Font Issues Found
- Hardcoded sizes or missing Dynamic Type support
- Suggested fixes
✅ Accessibility
- Well-implemented accessibility features
- Good label/hint usage
⚠️ Accessibility Issues Found
- Missing labels or hints
- Incorrect traits
- Navigation problems
- Suggested fixes with code examples
📋 Testing Recommendations
- Specific tests to run (VoiceOver, Dynamic Type, Dark Mode)
- Accessibility Inspector checks
- Device/simulator testing suggestions
Example Review Output
Reviewing: AddOrUpdateExpenseView.swift
✅ HIG Compliance
- Good use of semantic colors throughout
- Proper NavigationStack implementation
- Safe area handling is correct
⚠️ HIG Issues Found
1. AddOrUpdateExpenseView.swift:145 - Delete button tap target may be small
Suggested fix: Ensure .frame(minWidth: 44, minHeight: 44)
2. AddOrUpdateExpenseView.swift:203 - Hardcoded color
Current: .foregroundColor(.red)
Suggested: .foregroundColor(Color(.systemRed))
✅ Font Usage
- Excellent use of .headline for section headers
- Proper .body for content text
⚠️ Font Issues Found
1. AddOrUpdateExpenseView.swift:178 - Hardcoded font size
Current: .font(.system(size: 14))
Suggested: .font(.subheadline)
✅ Accessibility
- Good labels on most form fields
- Proper form structure
⚠️ Accessibility Issues Found
1. AddOrUpdateExpenseView.swift:92 - Icon button missing label
Current: Button { } label: { Image(systemName: "calendar") }
Suggested: Add .accessibilityLabel("Select date")
📋 Testing Recommendations
1. Test with VoiceOver enabled
2. Test at largest Dynamic Type size (Accessibility → Display)
3. Verify in Dark Mode
4. Use Accessibility Inspector to check contrast ratiosReferences
Always reference these when in doubt:
Notes
- Be constructive and specific
- Provide code examples for fixes
- Reference exact line numbers
- Prioritize user-impacting issues
- Consider context (some exceptions are valid)
Accessibility Quick Reference
Quick reference for SwiftUI accessibility implementation.
Essential Modifiers
Labels & Descriptions
// Label: What the element is
.accessibilityLabel("Add new expense")
// Value: Current state/value
.accessibilityValue("$150.00")
// Hint: What happens when activated
.accessibilityHint("Creates a new expense in this group")Traits
// Add traits
.accessibilityAddTraits(.isButton)
.accessibilityAddTraits(.isHeader)
.accessibilityAddTraits(.isSelected)
.accessibilityAddTraits(.updatesFrequently)
// Remove traits
.accessibilityRemoveTraits(.isImage)Grouping & Hiding
// Combine multiple elements into one
VStack {
Text("Total")
Text("$150")
}
.accessibilityElement(children: .combine)
// Hide decorative elements
Image("background-pattern")
.accessibilityHidden(true)Custom Actions
.accessibilityAction(named: "Delete") {
deleteExpense()
}
.accessibilityAction(named: "Edit") {
showEditSheet()
}Common Patterns
Icon-Only Buttons
// ❌ Bad
Button {
addExpense()
} label: {
Image(systemName: "plus")
}
// ✅ Good
Button {
addExpense()
} label: {
Image(systemName: "plus")
}
.accessibilityLabel("Add expense")Custom Controls
// ❌ Bad - VoiceOver doesn't know it's tappable
Image(systemName: "star")
.onTapGesture { toggleFavorite() }
// ✅ Good
Image(systemName: "star")
.onTapGesture { toggleFavorite() }
.accessibilityAddTraits(.isButton)
.accessibilityLabel("Favorite")
.accessibilityValue(isFavorite ? "On" : "Off")Lists with Actions
List {
ForEach(expenses) { expense in
ExpenseRow(expense: expense)
.accessibilityAction(named: "Delete") {
delete(expense)
}
.accessibilityAction(named: "Edit") {
edit(expense)
}
}
}Toggle States
Toggle("Enable notifications", isOn: $notificationsEnabled)
.accessibilityValue(notificationsEnabled ? "Enabled" : "Disabled")Progress & Status
ProgressView("Loading expenses", value: progress, total: 1.0)
.accessibilityLabel("Loading")
.accessibilityValue("\(Int(progress * 100)) percent complete")Dynamic Type Support
Use Semantic Text Styles
// ✅ Good - Scales automatically
Text("Expense Title")
.font(.headline)
Text("Description")
.font(.body)
Text("Date")
.font(.caption)
// ❌ Bad - Fixed size
Text("Expense Title")
.font(.system(size: 18))Custom Fonts with Scaling
// ✅ Good - Custom font that scales
Text("Title")
.font(.custom("SF Pro Display", size: 17, relativeTo: .body))
// ❌ Bad - Fixed custom font
Text("Title")
.font(.custom("SF Pro Display", fixedSize: 17))Handle Large Text
// Use ViewThatFits for flexibility
ViewThatFits {
HStack {
Text("Long text here")
Spacer()
Text("$150")
}
VStack(alignment: .leading) {
Text("Long text here")
Text("$150")
}
}
// Or use dynamic layout
@Environment(\.sizeCategory) var sizeCategory
var body: some View {
if sizeCategory.isAccessibilityCategory {
VStack { /* Vertical layout */ }
} else {
HStack { /* Horizontal layout */ }
}
}Color & Contrast
Semantic Colors
// ✅ Good - Adapts to light/dark mode
Text("Title")
.foregroundColor(.primary)
Background()
.fill(Color(.systemBackground))
// ❌ Bad - Fixed colors
Text("Title")
.foregroundColor(.black)
Background()
.fill(Color.white)Contrast Requirements
- Normal text: 4.5:1 contrast ratio
- Large text (18pt+): 3:1 contrast ratio
- UI components: 3:1 contrast ratio
Use Xcode's Accessibility Inspector to verify.
Testing Checklist
VoiceOver Testing
- [ ] All interactive elements have labels
- [ ] Navigation order is logical
- [ ] Custom actions work correctly
- [ ] Dynamic content is announced
Dynamic Type Testing
- [ ] Test at largest size (Accessibility XXXL)
- [ ] No clipped text
- [ ] Layouts adapt appropriately
- [ ] Buttons remain tappable
Visual Testing
- [ ] Test in Dark Mode
- [ ] Test with Increased Contrast
- [ ] Test with Reduce Transparency
- [ ] Test with Reduce Motion
Keyboard Testing (iPad/Mac)
- [ ] All actions have keyboard shortcuts
- [ ] Focus indicator is visible
- [ ] Tab order is logical
Testing in Simulator
Enable Accessibility Features
Settings → Accessibility → VoiceOver → On
Settings → Accessibility → Display & Text Size → Larger Text
Settings → Accessibility → Display & Text Size → Increase Contrast
Settings → Accessibility → Motion → Reduce MotionXcode Accessibility Inspector
Xcode → Open Developer Tool → Accessibility InspectorFeatures:
- Inspect element accessibility properties
- Audit for common issues
- Check color contrast
- Test with different settings
Resources
Font Usage Guidelines
Comprehensive guide for font usage, Dynamic Type, and typography best practices.
System Text Styles
iOS Text Style Hierarchy
| Style | Use Case | Default Size |
|---|---|---|
.largeTitle | Page titles, main headings | 34pt |
.title | Primary section headers | 28pt |
.title2 | Secondary section headers | 22pt |
.title3 | Tertiary section headers | 20pt |
.headline | Emphasized content, row titles | 17pt (semibold) |
.body | Primary content, body text | 17pt |
.callout | Secondary content | 16pt |
.subheadline | Secondary information | 15pt |
.footnote | Tertiary information, metadata | 13pt |
.caption | Image captions, labels | 12pt |
.caption2 | Secondary captions | 11pt |
Usage Examples
// ✅ Page title
Text("Expenses")
.font(.largeTitle)
// ✅ Section header
Text("Recent")
.font(.headline)
// ✅ Body content
Text("This is the main content of the expense description...")
.font(.body)
// ✅ Metadata
Text("Updated 2 hours ago")
.font(.caption)
.foregroundColor(.secondary)Dynamic Type Support
Why Dynamic Type Matters
- Accessibility: Users with vision impairments need larger text
- User Preference: Some prefer smaller/larger text
- App Store Requirement: Required for accessibility compliance
- Better UX: Respects user system settings
Automatic Dynamic Type
// ✅ Automatically scales with Dynamic Type
Text("Title")
.font(.headline)
Text("Body text here")
.font(.body)
// All system text styles scale automaticallyCustom Fonts with Dynamic Type
// ❌ Bad - Fixed size
Text("Custom Title")
.font(.custom("SF Pro Display", size: 20))
// ✅ Good - Scales with Dynamic Type
Text("Custom Title")
.font(.custom("SF Pro Display", size: 20, relativeTo: .headline))
// ✅ Alternative approach
Text("Custom Title")
.font(.custom("SF Pro Display", fixedSize: 20))
.dynamicTypeSize(.large...(.xxxLarge)) // Limit scaling range if neededTesting Dynamic Type
// Test different sizes in preview
struct ExpenseView_Previews: PreviewProvider {
static var previews: some View {
Group {
ExpenseView()
.previewDisplayName("Default")
ExpenseView()
.environment(\.sizeCategory, .extraSmall)
.previewDisplayName("Extra Small")
ExpenseView()
.environment(\.sizeCategory, .accessibilityExtraExtraExtraLarge)
.previewDisplayName("XXXL")
}
}
}Responsive Layouts for Large Text
// ❌ Bad - Layout breaks with large text
HStack {
Text("Very long expense name here")
Spacer()
Text("$150.00")
}
// ✅ Good - Adaptive layout
ViewThatFits {
HStack {
Text(expense.name)
Spacer()
Text(expense.formattedAmount)
}
VStack(alignment: .leading) {
Text(expense.name)
Text(expense.formattedAmount)
}
}
// ✅ Alternative - Manual check
@Environment(\.sizeCategory) var sizeCategory
var body: some View {
if sizeCategory.isAccessibilityCategory {
VStack(alignment: .leading, spacing: 4) {
Text(expense.name)
Text(expense.formattedAmount)
}
} else {
HStack {
Text(expense.name)
Spacer()
Text(expense.formattedAmount)
}
}
}Dynamic Type Size Categories
extension DynamicTypeSize {
var isAccessibilitySize: Bool {
self >= .accessibility1
}
}
// Size categories:
// .xSmall
// .small
// .medium (default)
// .large
// .xLarge
// .xxLarge
// .xxxLarge
// .accessibility1
// .accessibility2
// .accessibility3
// .accessibility4
// .accessibility5Limiting Dynamic Type Range
// ✅ Limit scaling range when necessary
Text("Icon Label")
.font(.caption)
.dynamicTypeSize(.small ... .large)
// ✅ Good for fixed-size UI elements
Button {
// action
} label: {
Image(systemName: "plus")
Text("Add")
.dynamicTypeSize(.small ... .large) // Keep button compact
}Font Weights & Styles
Font Weights
// Available weights
.font(.body.weight(.ultraLight))
.font(.body.weight(.thin))
.font(.body.weight(.light))
.font(.body.weight(.regular))
.font(.body.weight(.medium))
.font(.body.weight(.semibold))
.font(.body.weight(.bold))
.font(.body.weight(.heavy))
.font(.body.weight(.black))
// Shorthand
.font(.headline.bold())Font Styles
// Italic
.font(.body.italic())
// Monospaced
.font(.body.monospaced())
// Monospaced digit (for aligned numbers)
.font(.body.monospacedDigit())
// Small caps
.font(.body.smallCaps())
// Leading (line spacing)
.font(.body.leading(.tight))
.font(.body.leading(.standard))
.font(.body.leading(.loose))When to Use Font Weights
// ✅ Use semibold for emphasis
Text("Important Message")
.font(.body.weight(.semibold))
// ✅ Use bold for strong emphasis
Text("Warning")
.font(.body.bold())
// ❌ Avoid overuse of heavy weights
Text("Regular text")
.font(.body.weight(.black)) // Too heavy for body textText Formatting
Line Limit
// ✅ Limit lines for truncation
Text("Long text here...")
.lineLimit(2)
// ✅ Single line
Text("Title")
.lineLimit(1)
// ✅ Unlimited lines (default)
Text("Description")
.lineLimit(nil)Truncation Mode
// Truncate at tail (default)
Text("Very long text...")
.truncationMode(.tail)
// Truncate at head
Text("...end of text")
.truncationMode(.head)
// Truncate in middle
Text("Beginning...end")
.truncationMode(.middle)Text Alignment
// Left aligned (default)
Text("Left")
.multilineTextAlignment(.leading)
// Center aligned
Text("Center")
.multilineTextAlignment(.center)
// Right aligned
Text("Right")
.multilineTextAlignment(.trailing)Text Case
// Uppercase
Text("title")
.textCase(.uppercase) // "TITLE"
// Lowercase
Text("TITLE")
.textCase(.lowercase) // "title"
// None (preserve original)
Text("Title")
.textCase(nil)Common Patterns
Expense Row Typography
struct ExpenseRow: View {
let expense: Expense
var body: some View {
HStack(alignment: .firstTextBaseline) {
VStack(alignment: .leading, spacing: 4) {
Text(expense.name)
.font(.headline)
.lineLimit(1)
Text(expense.category)
.font(.subheadline)
.foregroundColor(.secondary)
}
Spacer()
VStack(alignment: .trailing, spacing: 4) {
Text(expense.formattedAmount)
.font(.body.monospacedDigit())
.fontWeight(.semibold)
Text(expense.date, style: .date)
.font(.caption)
.foregroundColor(.secondary)
}
}
.padding(.vertical, 4)
}
}Section Header
struct SectionHeaderView: View {
let title: String
var body: some View {
Text(title)
.font(.headline)
.foregroundColor(.primary)
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.vertical, 8)
}
}Empty State Typography
struct EmptyStateView: View {
var body: some View {
VStack(spacing: 16) {
Image(systemName: "tray.fill")
.font(.system(size: 60))
.foregroundColor(.secondary)
Text("No Expenses")
.font(.title2)
.fontWeight(.semibold)
Text("Add your first expense to get started")
.font(.body)
.foregroundColor(.secondary)
.multilineTextAlignment(.center)
}
.padding()
}
}Form Label
struct FormRow: View {
let label: String
let value: String
var body: some View {
HStack {
Text(label)
.font(.body)
.foregroundColor(.primary)
Spacer()
Text(value)
.font(.body)
.foregroundColor(.secondary)
}
}
}Anti-Patterns
Fixed Font Sizes
// ❌ Bad - Doesn't scale
Text("Title")
.font(.system(size: 18))
// ✅ Good - Scales with Dynamic Type
Text("Title")
.font(.headline)Mixing Styles Inconsistently
// ❌ Bad - Inconsistent hierarchy
VStack {
Text("Header 1").font(.title)
Text("Header 2").font(.headline)
Text("Header 3").font(.title2) // Wrong order
}
// ✅ Good - Consistent hierarchy
VStack {
Text("Header 1").font(.title)
Text("Header 2").font(.title2)
Text("Header 3").font(.headline)
}Too Many Font Weights
// ❌ Bad - Visual noise
VStack {
Text("Title").bold()
Text("Subtitle").fontWeight(.semibold)
Text("Description").fontWeight(.medium)
Text("Footer").bold()
}
// ✅ Good - Clear hierarchy
VStack {
Text("Title").bold()
Text("Subtitle").font(.subheadline)
Text("Description").font(.body)
Text("Footer").font(.caption).foregroundColor(.secondary)
}Ignoring Line Limits
// ❌ Bad - Can overflow
HStack {
Text("Very long expense name that might wrap")
Text("$150")
}
// ✅ Good - Controlled truncation
HStack {
Text("Very long expense name that might wrap")
.lineLimit(1)
Spacer()
Text("$150")
}Accessibility Considerations
Text Scaling Checklist
- [ ] All text uses system text styles or custom fonts with
relativeTo: - [ ] Layouts adapt to larger text sizes
- [ ] No clipped text at XXXL sizes
- [ ] Critical information visible at all sizes
- [ ] Buttons remain tappable at all sizes
Color & Contrast
- [ ] Text has sufficient contrast with background
- [ ] Secondary text still readable
- [ ] Don't rely on color alone for meaning
Testing Tools
Settings → Accessibility → Display & Text Size:
- Larger Text
- Bold Text
- Increase Contrast
Xcode Accessibility Inspector:
- Text size preview
- Contrast analyzer
- Layout viewer
Font Naming Conventions
Variable Names
// ✅ Clear naming
let titleFont: Font = .headline
let bodyFont: Font = .body
let captionFont: Font = .caption
// ❌ Unclear naming
let font1: Font = .headline
let bigFont: Font = .titleCustom Text Styles
// ✅ Reusable text styles
struct TextStyles {
static let pageTitle: Font = .largeTitle.bold()
static let sectionHeader: Font = .headline
static let emphasized: Font = .body.weight(.semibold)
static let metadata: Font = .caption
}
// Usage
Text("Page Title")
.font(TextStyles.pageTitle)References
Human Interface Guidelines Checklist
Comprehensive checklist for iOS and watchOS HIG compliance.
iOS Human Interface Guidelines
Layout & Spacing
Safe Areas
- [ ] Content respects safe area insets
- [ ]
.safeAreaInset()used for persistent content - [ ]
.ignoresSafeArea()only for backgrounds/media
// ✅ Good
VStack {
content
}
.safeAreaInset(edge: .bottom) {
BottomBar()
}
// ✅ Good - Background extends
ZStack {
Color.blue
.ignoresSafeArea()
content
}Tap Targets
- [ ] Minimum 44x44 points for all interactive elements
- [ ] Adequate spacing between tappable elements
// ✅ Ensure minimum size
Button("Action") { }
.frame(minWidth: 44, minHeight: 44)Spacing
- [ ] Consistent spacing using system values
- [ ]
.padding()for standard spacing - [ ]
.padding(.horizontal)/.padding(.vertical)for directional
// ✅ Use system spacing
VStack(spacing: 16) {
// Standard spacing
}
// ✅ Use padding
content
.padding()ScrollView
- [ ] Long content uses ScrollView
- [ ] Scroll indicators visible
- [ ] Content offset considered
Navigation
NavigationStack (iOS 16+)
- [ ] Use NavigationStack instead of deprecated NavigationView
- [ ] Proper navigation title placement
- [ ] Back button navigation works correctly
// ✅ Modern navigation
NavigationStack {
List(items) { item in
NavigationLink(value: item) {
ItemRow(item: item)
}
}
.navigationTitle("Items")
.navigationBarTitleDisplayMode(.large)
.navigationDestination(for: Item.self) { item in
ItemDetailView(item: item)
}
}Navigation Patterns
- [ ] Clear navigation hierarchy
- [ ] Proper use of navigation modifiers
- [ ] Toolbar items placed appropriately
// ✅ Toolbar placement
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button("Add") { }
}
}Modality
Sheets
- [ ] Sheets for focused tasks
- [ ] Dismiss gesture available
- [ ] Proper presentation detents on iOS 16+
// ✅ Proper sheet
.sheet(isPresented: $showingSheet) {
AddItemView()
.presentationDetents([.medium, .large])
.presentationDragIndicator(.visible)
}Alerts
- [ ] Destructive actions marked with role
- [ ] Cancel button provided
- [ ] Clear, concise messaging
// ✅ Proper alert
.alert("Delete Item?", isPresented: $showingAlert) {
Button("Delete", role: .destructive) {
deleteItem()
}
Button("Cancel", role: .cancel) { }
} message: {
Text("This action cannot be undone.")
}Confirmation Dialogs
- [ ] Use for multiple action choices
- [ ] Destructive actions at top
- [ ] Cancel at bottom
// ✅ Confirmation dialog
.confirmationDialog("Options", isPresented: $showingOptions) {
Button("Delete", role: .destructive) { }
Button("Archive") { }
Button("Cancel", role: .cancel) { }
}Colors & Visuals
Semantic Colors
- [ ] Use semantic color names
- [ ] Avoid hardcoded colors
- [ ] Support both light and dark mode
// ✅ Semantic colors
.foregroundColor(.primary) // Adapts to theme
.foregroundColor(.secondary)
.background(Color(.systemBackground))
// ❌ Hardcoded colors
.foregroundColor(.black) // Doesn't adapt
.background(.white)System Colors
- [ ] Use UIColor system colors via Color()
- [ ] Proper contrast in both modes
// ✅ System colors
Color(.systemRed)
Color(.systemBackground)
Color(.secondarySystemBackground)
Color(.label)
Color(.secondaryLabel)Color Contrast
- [ ] Normal text: 4.5:1 minimum
- [ ] Large text (18pt+): 3.1 minimum
- [ ] UI components: 3:1 minimum
- [ ] Verify with Accessibility Inspector
SF Symbols
- [ ] Use SF Symbols for icons
- [ ] Consistent symbol style
- [ ] Proper symbol rendering mode
// ✅ SF Symbols
Image(systemName: "plus.circle.fill")
.symbolRenderingMode(.hierarchical)
.imageScale(.large)Lists & Collections
List Performance
- [ ] Use
idfor stable identities - [ ] Provide IDs or use Identifiable
- [ ] Efficient row updates
// ✅ Stable IDs
List(items, id: \.id) { item in
ItemRow(item: item)
}
// ✅ Or Identifiable
struct Item: Identifiable {
let id: UUID
}
List(items) { item in
ItemRow(item: item)
}List Actions
- [ ] Swipe actions for common operations
- [ ] Context menus for additional actions
- [ ] Pull to refresh when appropriate
// ✅ Swipe actions
.swipeActions(edge: .trailing) {
Button("Delete", role: .destructive) { }
}
// ✅ Context menu
.contextMenu {
Button("Edit") { }
Button("Delete", role: .destructive) { }
}Forms & Input
Form Structure
- [ ] Sections with headers
- [ ] Grouped related fields
- [ ] Clear labels
// ✅ Well-structured form
Form {
Section("Basic Info") {
TextField("Name", text: $name)
TextField("Email", text: $email)
}
Section("Preferences") {
Toggle("Notifications", isOn: $notifications)
Picker("Theme", selection: $theme) {
ForEach(Theme.allCases) { theme in
Text(theme.rawValue).tag(theme)
}
}
}
}Input Validation
- [ ] Real-time validation feedback
- [ ] Clear error messages
- [ ] Disable submit until valid
// ✅ Validation feedback
TextField("Email", text: $email)
.textInputAutocapitalization(.never)
.keyboardType(.emailAddress)
.overlay(alignment: .trailing) {
if !email.isEmpty && !isValidEmail {
Image(systemName: "xmark.circle.fill")
.foregroundColor(.red)
}
}Loading & Empty States
Loading States
- [ ] ProgressView for loading
- [ ] Descriptive labels
- [ ] Consider skeleton screens
// ✅ Loading state
if isLoading {
ProgressView("Loading items...")
} else {
ContentView()
}Empty States
- [ ] Helpful message when empty
- [ ] Action to add first item
- [ ] Icon or illustration
// ✅ Empty state
if items.isEmpty {
ContentUnavailableView(
"No Items",
systemImage: "tray.fill",
description: Text("Add your first item to get started")
)
} else {
List(items) { item in
ItemRow(item: item)
}
}Error States
- [ ] Clear error messaging
- [ ] Retry action
- [ ] Contact support option
// ✅ Error state
ContentUnavailableView(
"Unable to Load",
systemImage: "exclamationmark.triangle",
description: Text("Please check your connection and try again")
) {
Button("Retry") {
loadData()
}
}watchOS Human Interface Guidelines
Layout
Screen Sizes
- [ ] Support all watch sizes (38mm to 49mm)
- [ ] Test on different screen sizes
- [ ] Responsive layouts
Padding & Spacing
- [ ] Larger padding than iOS (content closer to edges)
- [ ] Clear visual hierarchy
- [ ] Generous tap targets (40x40 minimum)
Scrolling
- [ ] Use Lists for scrollable content
- [ ] Digital Crown scroll support
- [ ] Minimal horizontal scrolling
Interaction
Digital Crown
- [ ] Support Digital Crown for scrolling
- [ ] Use for value input when appropriate
- [ ] Provide visual feedback
// ✅ Digital Crown input
@State private var value = 0.5
var body: some View {
VStack {
Text("Value: \(value, specifier: "%.2f")")
Gauge(value: value, in: 0...1) {
Text("Amount")
}
}
.focusable()
.digitalCrownRotation($value)
}Tap Targets
- [ ] Minimum 40x40 points
- [ ] Full-width buttons when possible
- [ ] Clear spacing between elements
Complications
- [ ] Provide complications for quick access
- [ ] Update complications regularly
- [ ] Support multiple complication families
Navigation
Hierarchical Navigation
- [ ] Clear navigation stack
- [ ] Back navigation with edge swipe
- [ ] Navigation titles
// ✅ watchOS navigation
NavigationStack {
List(items) { item in
NavigationLink(value: item) {
Text(item.name)
}
}
.navigationTitle("Items")
.navigationDestination(for: Item.self) { item in
ItemDetailView(item: item)
}
}Pages (Tab-like)
- [ ] Use TabView for page-based navigation
- [ ] Index dots visible
- [ ] Swipe between pages
// ✅ Page-based navigation
TabView {
ActivityView()
SummaryView()
SettingsView()
}
.tabViewStyle(.page)Text & Fonts
Font Sizes
- [ ] Larger than iOS equivalents
- [ ] Support Dynamic Type
- [ ] Test at largest sizes
// ✅ Appropriate watchOS fonts
Text("Title")
.font(.headline)
Text("Body")
.font(.body)Text Length
- [ ] Keep text concise
- [ ] Truncate appropriately
- [ ] Use abbreviations when sensible
Color & Contrast
High Contrast
- [ ] Higher contrast than iOS
- [ ] Test in both always-on modes
- [ ] Consider outdoor visibility
Always-On Display
- [ ] Dimmed content for always-on
- [ ] Remove unnecessary animations
- [ ] Reduce color saturation
Testing Checklist
Device Testing
- [ ] Test on smallest device
- [ ] Test on largest device
- [ ] Test on mid-size devices
Orientation
- [ ] iPad: All orientations
- [ ] iPhone: Portrait and landscape
- [ ] Adaptive layouts
Accessibility
- [ ] VoiceOver navigation
- [ ] Dynamic Type scaling
- [ ] Reduce Motion
- [ ] Increase Contrast
Visual Modes
- [ ] Light mode
- [ ] Dark mode
- [ ] High contrast mode
- [ ] Reduce transparency