| name | auditing-accessibility |
| description | Comprehensive accessibility audit for iOS/macOS apps - VoiceOver, Dynamic Type, color contrast, touch targets, keyboard navigation, Reduce Motion, and App Store Review preparation |
Accessibility Audit
Perform a systematic accessibility audit for iOS/macOS apps. This audit covers the 7 most common accessibility issues that cause App Store rejections and user complaints.
Scope (optional): $ARGUMENTS
If no scope is provided, "all" is assumed.
Step 1: Identify Files to Audit
Search for SwiftUI views and UIKit view controllers in the target path:
find {target_path} -name "*.swift" | xargs grep -l "View\|ViewController"
For each file found, proceed through Steps 2-8.
Step 2: Audit VoiceOver Labels & Hints (CRITICAL - App Store Rejection)
WCAG 4.1.2 Name, Role, Value (Level A)
Search for these violations in each file:
2.1 Find icon-only buttons without labels
Grep: Button.*Image\(systemName:
Violations:
Button(action: addToCart) {
Image(systemName: "cart.badge.plus")
}
.accessibilityLabel("Button")
.accessibilityLabel("cart.badge.plus")
Fixes:
Button(action: addToCart) {
Image(systemName: "cart.badge.plus")
}
.accessibilityLabel("Add to cart")
.accessibilityLabel("Add to cart")
.accessibilityHint("Double-tap to add this item to your shopping cart")
2.2 Find decorative images that should be hidden
Grep: Image\(".*"\)(?!.*accessibility)
Fixes:
Image("decorative-pattern")
.accessibilityHidden(true)
HStack {
Image(systemName: "star.fill")
Text("4.5")
Text("(234 reviews)")
}
.accessibilityElement(children: .combine)
.accessibilityLabel("Rating: 4.5 stars from 234 reviews")
2.3 When to use hints
- Action is not obvious from label ("Add to cart" is obvious, no hint needed)
- Multi-step interaction ("Swipe right to confirm, left to cancel")
- State change ("Double-tap to toggle notifications on or off")
Report format:
VoiceOver Issues Found:
- [ ] {file}:{line} - Button with Image missing accessibilityLabel
- [ ] {file}:{line} - Decorative image not hidden from VoiceOver
- [ ] {file}:{line} - Generic "Button" label
Step 3: Audit Dynamic Type Support (HIGH - User Experience)
WCAG 1.4.4 Resize Text (Level AA - support 200% scaling)
3.1 Find fixed font sizes
Grep: \.font\(\.system\(size:
Grep: UIFont\.systemFont\(ofSize:
Grep: Font\.custom\(.*size:
Violations:
Text("Price: $19.99")
.font(.system(size: 17))
UILabel().font = UIFont.systemFont(ofSize: 17)
Text("Headline")
.font(Font.custom("CustomFont", size: 24))
Fixes:
Text("Price: $19.99")
.font(.body)
Text("Headline")
.font(.headline)
label.font = UIFont.preferredFont(forTextStyle: .body)
let customFont = UIFont(name: "CustomFont", size: 24)!
label.font = UIFontMetrics.default.scaledFont(for: customFont)
label.adjustsFontForContentSizeCategory = true
Text("Large Title")
.font(.system(size: 60).relativeTo(.largeTitle))
Text("Custom Headline")
.font(.system(size: 24).relativeTo(.title2))
3.2 SwiftUI text styles reference
.largeTitle - 34pt (scales to 44pt at accessibility sizes)
.title - 28pt
.title2 - 22pt
.title3 - 20pt
.headline - 17pt semibold
.body - 17pt (default)
.callout - 16pt
.subheadline - 15pt
.footnote - 13pt
.caption - 12pt
.caption2 - 11pt
3.3 Find fixed frames that clip text
Grep: \.frame\(.*height:.*\d+
Violations:
Text("Long product description...")
.font(.body)
.frame(height: 50)
Fixes:
Text("Long product description...")
.font(.body)
.lineLimit(nil)
.fixedSize(horizontal: false, vertical: true)
HStack {
Text("Label:")
Text("Value")
}
.dynamicTypeSize(...DynamicTypeSize.xxxLarge)
Report format:
Dynamic Type Issues Found:
- [ ] {file}:{line} - Fixed font size .system(size: 17)
- [ ] {file}:{line} - Fixed frame height will clip at large text
- [ ] {file}:{line} - Custom font without UIFontMetrics scaling
Step 4: Audit Color Contrast (HIGH - Vision Disabilities)
WCAG
- 1.4.3 Contrast (Minimum) — Level AA: Normal text 4.5:1, Large text 3:1
- 1.4.6 Contrast (Enhanced) — Level AAA: Normal text 7:1, Large text 4.5:1
4.1 Find low-contrast color combinations
Grep: \.foregroundColor\(\.yellow
Grep: \.foregroundColor\(\.gray\)
Grep: \.foregroundColor\(Color\(
Violations:
Text("Warning")
.foregroundColor(.yellow)
Text("Info")
.foregroundColor(.gray)
Fixes:
Text("Warning")
.foregroundColor(.orange)
Text("Info")
.foregroundColor(.primary)
Text("Secondary")
.foregroundColor(.secondary)
4.2 Find color-only status indicators
Grep: \.fill\(.*\.green.*\.red
Grep: \.foregroundColor\(.*\?.*:
Violations:
Circle()
.fill(isAvailable ? .green : .red)
Fixes:
HStack {
Image(systemName: isAvailable ? "checkmark.circle.fill" : "xmark.circle.fill")
Text(isAvailable ? "Available" : "Unavailable")
}
.foregroundColor(isAvailable ? .green : .red)
if UIAccessibility.shouldDifferentiateWithoutColor {
}
4.3 Contrast ratio reference
- Black (#000000) on White (#FFFFFF): 21:1 ✅ AAA
- Dark Gray (#595959) on White: 7:1 ✅ AAA
- Medium Gray (#767676) on White: 4.5:1 ✅ AA
- Light Gray (#959595) on White: 2.8:1 ❌ Fails
Report format:
Color Contrast Issues Found:
- [ ] {file}:{line} - .yellow foreground likely fails 4.5:1 contrast
- [ ] {file}:{line} - Color-only status indicator (green/red)
- [ ] {file}:{line} - Custom color needs contrast verification
Step 5: Audit Touch Target Sizes (MEDIUM - Motor Disabilities)
WCAG 2.5.5 Target Size (Level AAA - 44x44pt minimum)
Apple HIG 44x44pt minimum for all tappable elements
5.1 Find small touch targets
Grep: \.frame\(width:\s*\d+,\s*height:\s*\d+
Grep: \.frame\(.*[0-3]\d.*[0-3]\d
Grep: \.onTapGesture.*Image\(systemName
Violations:
Button("×") {
dismiss()
}
.frame(width: 24, height: 24)
Image(systemName: "heart")
.font(.system(size: 16))
.onTapGesture { }
Fixes:
Button("×") {
dismiss()
}
.frame(minWidth: 44, minHeight: 44)
Image(systemName: "heart")
.font(.system(size: 24))
.frame(minWidth: 44, minHeight: 44)
.contentShape(Rectangle())
.onTapGesture { }
button.contentEdgeInsets = UIEdgeInsets(top: 12, left: 12, bottom: 12, right: 12)
5.2 Find closely spaced targets
Grep: HStack\(spacing:\s*[0-7]\)
Grep: VStack\(spacing:\s*[0-7]\)
Violations:
HStack(spacing: 4) {
Button("Edit") { }
Button("Delete") { }
}
Fixes:
HStack(spacing: 12) {
Button("Edit") { }
Button("Delete") { }
}
Report format:
Touch Target Issues Found:
- [ ] {file}:{line} - Button frame 24x24 (needs 44x44 minimum)
- [ ] {file}:{line} - HStack spacing 4pt (needs 8pt+ between buttons)
- [ ] {file}:{line} - Icon with onTapGesture missing contentShape
Step 6: Audit Keyboard Navigation (MEDIUM - iPadOS/macOS)
WCAG 2.1.1 Keyboard (Level A - all functionality via keyboard)
6.1 Find gesture-only interactions
Grep: \.onTapGesture
Grep: \.gesture\(
Violations:
.onTapGesture {
showDetails()
}
Fixes:
Button("Show Details") {
showDetails()
}
.keyboardShortcut("d", modifiers: .command)
struct CustomButton: View {
@FocusState private var isFocused: Bool
var body: some View {
Text("Custom")
.focusable()
.focused($isFocused)
.onKeyPress(.return) {
action()
return .handled
}
}
}
6.2 Focus management patterns
.focusSection()
.defaultFocus($focus, .constant(true))
@FocusState private var focusedField: Field?
Button("Next") {
focusedField = .next
}
Report format:
Keyboard Navigation Issues Found:
- [ ] {file}:{line} - onTapGesture without Button alternative
- [ ] {file}:{line} - Custom control missing focusable()
- [ ] {file}:{line} - Form without focus management
Step 7: Audit Reduce Motion Support (MEDIUM - Vestibular Disorders)
WCAG 2.3.3 Animation from Interactions (Level AAA)
7.1 Find animations without Reduce Motion check
Grep: withAnimation\(
Grep: \.animation\(
Grep: \.transition\(
Grep: \.offset.*GeometryReader
Violations:
.onAppear {
withAnimation(.spring(response: 0.6, dampingFraction: 0.8)) {
scale = 1.0
}
}
ScrollView {
GeometryReader { geo in
Image("hero")
.offset(y: geo.frame(in: .global).minY * 0.5)
}
}
Fixes:
.onAppear {
if UIAccessibility.isReduceMotionEnabled {
scale = 1.0
} else {
withAnimation(.spring(response: 0.6, dampingFraction: 0.8)) {
scale = 1.0
}
}
}
if UIAccessibility.isReduceMotionEnabled {
withAnimation(.linear(duration: 0.2)) {
showView = true
}
} else {
withAnimation(.spring()) {
showView = true
}
}
.animation(.spring(), value: isExpanded)
.transaction { transaction in
if UIAccessibility.isReduceMotionEnabled {
transaction.animation = nil
}
}
Report format:
Reduce Motion Issues Found:
- [ ] {file}:{line} - withAnimation without isReduceMotionEnabled check
- [ ] {file}:{line} - Parallax effect without Reduce Motion opt-out
- [ ] {file}:{line} - Spring animation should use cross-fade when reduced
Step 8: Audit Common Violations (HIGH - App Store Review)
8.1 Images without accessibility
Grep: Image\(".*"\)(?!.*accessibility)
Grep: Image\(systemName:.*\)(?!.*accessibility)
Violations:
Image("product-photo")
Fixes:
Image("product-photo")
.accessibilityLabel("Red sneakers with white laces")
Image("background-pattern")
.accessibilityHidden(true)
8.2 Custom buttons without traits
Grep: Text\(.*\).*\.onTapGesture
Violations:
Text("Submit")
.onTapGesture {
submit()
}
Fixes:
Button("Submit") {
submit()
}
Text("Submit")
.accessibilityAddTraits(.isButton)
.onTapGesture {
submit()
}
8.3 Custom controls without accessibility actions
Grep: DragGesture\(\)
Grep: GeometryReader.*gesture
Violations:
struct CustomSlider: View {
@Binding var value: Double
var body: some View {
GeometryReader { geo in
}
.gesture(DragGesture()...)
}
}
Fixes:
struct CustomSlider: View {
@Binding var value: Double
var body: some View {
GeometryReader { geo in
}
.gesture(DragGesture()...)
.accessibilityElement()
.accessibilityLabel("Volume")
.accessibilityValue("\(Int(value))%")
.accessibilityAdjustableAction { direction in
switch direction {
case .increment:
value = min(value + 10, 100)
case .decrement:
value = max(value - 10, 0)
@unknown default:
break
}
}
}
}
8.4 State changes without announcements
Grep: \.toggle\(\)
Grep: isOn.*=.*!
Violations:
Button("Toggle") {
isOn.toggle()
}
Fixes:
Button("Toggle") {
isOn.toggle()
UIAccessibility.post(
notification: .announcement,
argument: isOn ? "Enabled" : "Disabled"
)
}
Button("Toggle") {
isOn.toggle()
}
.accessibilityValue(isOn ? "Enabled" : "Disabled")
Report format:
Common Violations Found:
- [ ] {file}:{line} - Image without accessibilityLabel or accessibilityHidden
- [ ] {file}:{line} - Text with onTapGesture missing .isButton trait
- [ ] {file}:{line} - Custom slider without accessibilityAdjustableAction
- [ ] {file}:{line} - State toggle without announcement
Step 9: Run Testing and Generate Report
See ./accessibility-testing-checklist.md for:
- Accessibility Inspector audit steps
- Manual VoiceOver testing checklists
- Audit report template
Reference Documents
For detailed reference material, see:
./wcag-reference.md — WCAG compliance levels (A, AA, AAA)
./app-store-accessibility-prep.md — App Store submission requirements
./design-pressure-red-flags.md — Designer pushback and resources
./accessibility-testing-checklist.md — Inspector audit and report template