Copilot, Claude, Cursor, and others all read from ~/.agents/. The npx skills CLI handles fan-out to tool-specific directories.
8.2 KiB
8.2 KiB
| name | description | globs | |
|---|---|---|---|
| SwiftUI Accessibility | Dynamic Type support and VoiceOver accessibility implementation |
|
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:
// 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:
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:
// 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
// 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:
// 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:
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:
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:
// 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:
// 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:
// 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:
ItemRow(item: item)
.accessibilityAction(named: "Delete") {
deleteItem(item)
}
.accessibilityAction(named: "Edit") {
editItem(item)
}
.accessibilityAction(named: "Share") {
shareItem(item)
}
Accessibility Announcements
Announce important state changes:
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:
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
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
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
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
- Settings → Accessibility → VoiceOver
- 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