--- name: SwiftUI Accessibility description: Dynamic Type support and VoiceOver accessibility implementation globs: ["**/*.swift"] --- # Accessibility: Dynamic Type and VoiceOver Accessibility is not optional. All apps must support Dynamic Type and VoiceOver. ## Dynamic Type ### Always Support Dynamic Type Use system text styles that scale automatically: ```swift // GOOD - Scales with Dynamic Type Text("Title") .font(.title) Text("Body text") .font(.body) Text("Caption") .font(.caption) ``` ### Use @ScaledMetric for Custom Dimensions When you need custom sizes that should scale with Dynamic Type: ```swift struct CustomCard: View { @ScaledMetric(relativeTo: .body) private var iconSize: CGFloat = 24 @ScaledMetric(relativeTo: .body) private var spacing: CGFloat = 12 @ScaledMetric(relativeTo: .title) private var headerHeight: CGFloat = 44 var body: some View { VStack(spacing: spacing) { Image(systemName: "star") .font(.system(size: iconSize)) Text("Content") } } } ``` ### Choose Appropriate relativeTo Styles Match the scaling behavior to the content's purpose: | Content Type | relativeTo | |-------------|-----------| | Body content spacing | `.body` | | Title decorations | `.title` | | Caption elements | `.caption` | | Large headers | `.largeTitle` | ### Fixed Sizes (Use Sparingly) Only use fixed sizes when absolutely necessary, and document the reason: ```swift // Fixed size for app icon badge - must match system badge size private let badgeSize: CGFloat = 24 // Fixed: matches system notification badge // Fixed for external API requirements private let avatarUploadSize: CGFloat = 256 // Fixed: server requires exactly 256x256 ``` ### Prefer System Text Styles ```swift // GOOD - System styles .font(.body) .font(.headline) .font(.title) .font(.caption) // AVOID - Custom sizes that don't scale .font(.system(size: 14)) // IF you must use custom sizes, use ScaledMetric @ScaledMetric private var customSize: CGFloat = 14 .font(.system(size: customSize)) ``` ## VoiceOver ### Accessibility Labels All interactive elements must have meaningful labels: ```swift // GOOD - Descriptive label Button { } label: { Image(systemName: "trash") } .accessibilityLabel("Delete item") // GOOD - Context-aware label Button { } label: { Image(systemName: "heart.fill") } .accessibilityLabel(item.isFavorite ? "Remove from favorites" : "Add to favorites") ``` ### Accessibility Values Use for dynamic state that changes: ```swift Slider(value: $volume) .accessibilityLabel("Volume") .accessibilityValue("\(Int(volume * 100)) percent") Toggle(isOn: $isEnabled) { Text("Notifications") } .accessibilityValue(isEnabled ? "On" : "Off") ``` ### Accessibility Hints Describe what happens when the user interacts: ```swift Button("Submit") { } .accessibilityLabel("Submit order") .accessibilityHint("Double-tap to place your order and proceed to payment") NavigationLink(value: item) { ItemRow(item: item) } .accessibilityHint("Opens item details") ``` ### Accessibility Traits Use traits to convey element type and behavior: ```swift // Button trait (usually automatic) Text("Tap me") .onTapGesture { } .accessibilityAddTraits(.isButton) // Header trait for section headers Text("Settings") .font(.headline) .accessibilityAddTraits(.isHeader) // Selected state ItemRow(item: item) .accessibilityAddTraits(isSelected ? .isSelected : []) // Image trait removal for decorative images Image("decorative-background") .accessibilityHidden(true) ``` ### Hide Decorative Elements Hide elements that don't provide meaningful information: ```swift // Decorative separator Divider() .accessibilityHidden(true) // Background decoration Image("pattern") .accessibilityHidden(true) // Redundant icon next to text HStack { Image(systemName: "envelope") .accessibilityHidden(true) // Label conveys the meaning Text("Email") } ``` ### Group Related Elements Reduce navigation complexity by grouping related content: ```swift // GOOD - Single VoiceOver element HStack { Image(systemName: "person") VStack(alignment: .leading) { Text(user.name) Text(user.email) .font(.caption) } } .accessibilityElement(children: .combine) // OR create a completely custom accessibility representation .accessibilityElement(children: .ignore) .accessibilityLabel("\(user.name), \(user.email)") ``` ### Accessibility Actions Add custom actions for complex interactions: ```swift ItemRow(item: item) .accessibilityAction(named: "Delete") { deleteItem(item) } .accessibilityAction(named: "Edit") { editItem(item) } .accessibilityAction(named: "Share") { shareItem(item) } ``` ### Accessibility Announcements Announce important state changes: ```swift func completeTask() { task.isCompleted = true // Announce the change AccessibilityNotification.Announcement("Task completed") .post() } func showError(_ message: String) { errorMessage = message // Announce errors immediately AccessibilityNotification.Announcement(message) .post() } ``` ### Accessibility Focus Control focus for important UI changes: ```swift struct ContentView: View { @AccessibilityFocusState private var isSearchFocused: Bool @State private var showingSearch = false var body: some View { VStack { if showingSearch { TextField("Search", text: $searchText) .accessibilityFocused($isSearchFocused) } Button("Search") { showingSearch = true isSearchFocused = true // Move focus to search field } } } } ``` ## Common Patterns ### Cards and List Items ```swift struct ItemCard: View { let item: Item var body: some View { VStack(alignment: .leading) { Text(item.title) .font(.headline) Text(item.subtitle) .font(.subheadline) .foregroundStyle(.secondary) HStack { Label("\(item.likes)", systemImage: "heart") Label("\(item.comments)", systemImage: "bubble.right") } .font(.caption) } // Combine into single VoiceOver element .accessibilityElement(children: .combine) // Add meaningful summary .accessibilityLabel("\(item.title), \(item.subtitle)") .accessibilityValue("\(item.likes) likes, \(item.comments) comments") } } ``` ### Interactive Charts ```swift Chart { ForEach(data) { point in LineMark(x: .value("Date", point.date), y: .value("Value", point.value)) } } .accessibilityLabel("Sales chart") .accessibilityValue("Showing data from \(startDate) to \(endDate)") .accessibilityHint("Swipe up or down to hear individual data points") .accessibilityChartDescriptor(self) ``` ### Custom Controls ```swift struct RatingControl: View { @Binding var rating: Int var body: some View { HStack { ForEach(1...5, id: \.self) { star in Image(systemName: star <= rating ? "star.fill" : "star") .onTapGesture { rating = star } } } .accessibilityElement(children: .ignore) .accessibilityLabel("Rating") .accessibilityValue("\(rating) of 5 stars") .accessibilityAdjustableAction { direction in switch direction { case .increment: rating = min(5, rating + 1) case .decrement: rating = max(1, rating - 1) @unknown default: break } } } } ``` ## Testing Accessibility ### Enable VoiceOver in Simulator 1. Settings → Accessibility → VoiceOver 2. Or use Accessibility Inspector (Xcode → Open Developer Tool) ### Audit Checklist - [ ] All interactive elements have labels - [ ] Dynamic content announces changes - [ ] Decorative elements are hidden - [ ] Text scales with Dynamic Type - [ ] Touch targets are at least 44pt - [ ] Color is not the only indicator of state - [ ] Groups reduce navigation complexity