Add scan flow MVP and local Axiom skill workspace
This snapshot establishes the camera-to-result recognition flow and related tests while checking in the project skill/docs assets required for the configured local tooling.
This commit is contained in:
7
.claude/skills/axiom-accessibility-diag/.openskills.json
Normal file
7
.claude/skills/axiom-accessibility-diag/.openskills.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"source": "CharlesWiltgen/Axiom",
|
||||
"sourceType": "git",
|
||||
"repoUrl": "https://github.com/CharlesWiltgen/Axiom",
|
||||
"subpath": "axiom-codex/skills/axiom-accessibility-diag",
|
||||
"installedAt": "2026-04-12T08:05:39.269Z"
|
||||
}
|
||||
989
.claude/skills/axiom-accessibility-diag/SKILL.md
Normal file
989
.claude/skills/axiom-accessibility-diag/SKILL.md
Normal file
@@ -0,0 +1,989 @@
|
||||
---
|
||||
name: axiom-accessibility-diag
|
||||
description: Use when fixing VoiceOver issues, Dynamic Type violations, color contrast failures, touch target problems, keyboard navigation gaps, or Reduce Motion support - comprehensive accessibility diagnostics with WCAG compliance, Accessibility Inspector workflows, and App Store Review preparation for iOS/macOS
|
||||
license: MIT
|
||||
metadata:
|
||||
version: "1.0.0"
|
||||
---
|
||||
|
||||
# Accessibility Diagnostics
|
||||
|
||||
## Overview
|
||||
|
||||
Systematic accessibility diagnosis and remediation for iOS/macOS apps. Covers the 7 most common accessibility issues that cause App Store rejections and user complaints.
|
||||
|
||||
**Core principle** Accessibility is not optional. iOS apps must support VoiceOver, Dynamic Type, and sufficient color contrast to pass App Store Review. Users with disabilities depend on these features.
|
||||
|
||||
## When to Use This Skill
|
||||
|
||||
- Fixing VoiceOver navigation issues (missing labels, wrong element order)
|
||||
- Supporting Dynamic Type (text scaling for vision disabilities)
|
||||
- Meeting color contrast requirements (WCAG AA/AAA)
|
||||
- Fixing touch target size violations (< 44x44pt)
|
||||
- Adding keyboard navigation (iPadOS/macOS)
|
||||
- Supporting Reduce Motion (vestibular disorders)
|
||||
- Preparing for App Store Review accessibility requirements
|
||||
- Responding to user complaints about accessibility
|
||||
|
||||
## The 7 Critical Accessibility Issues
|
||||
|
||||
### 1. VoiceOver Labels & Hints (CRITICAL - App Store Rejection)
|
||||
|
||||
**Problem** Missing or generic accessibility labels prevent VoiceOver users from understanding UI purpose.
|
||||
|
||||
**WCAG** 4.1.2 Name, Role, Value (Level A)
|
||||
|
||||
#### Common violations
|
||||
```swift
|
||||
// ❌ WRONG - No label (VoiceOver says "Button")
|
||||
Button(action: addToCart) {
|
||||
Image(systemName: "cart.badge.plus")
|
||||
}
|
||||
|
||||
// ❌ WRONG - Generic label
|
||||
.accessibilityLabel("Button")
|
||||
|
||||
// ❌ WRONG - Reads implementation details
|
||||
.accessibilityLabel("cart.badge.plus") // VoiceOver: "cart dot badge dot plus"
|
||||
|
||||
// ✅ CORRECT - Descriptive label
|
||||
Button(action: addToCart) {
|
||||
Image(systemName: "cart.badge.plus")
|
||||
}
|
||||
.accessibilityLabel("Add to cart")
|
||||
|
||||
// ✅ CORRECT - With hint for complex actions
|
||||
.accessibilityLabel("Add to cart")
|
||||
.accessibilityHint("Double-tap to add this item to your shopping cart")
|
||||
```
|
||||
|
||||
#### 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")
|
||||
|
||||
#### Decorative elements
|
||||
```swift
|
||||
// ✅ CORRECT - Hide decorative images from VoiceOver
|
||||
Image("decorative-pattern")
|
||||
.accessibilityHidden(true)
|
||||
|
||||
// ✅ CORRECT - Combine multiple elements into one label
|
||||
HStack {
|
||||
Image(systemName: "star.fill")
|
||||
Text("4.5")
|
||||
Text("(234 reviews)")
|
||||
}
|
||||
.accessibilityElement(children: .combine)
|
||||
.accessibilityLabel("Rating: 4.5 stars from 234 reviews")
|
||||
```
|
||||
|
||||
#### Testing
|
||||
- Enable VoiceOver: Cmd+F5 (simulator) or triple-click side button (device)
|
||||
- Navigate: Swipe right/left to move between elements
|
||||
- Listen: Does VoiceOver announce purpose clearly?
|
||||
- Check order: Does navigation order match visual layout?
|
||||
|
||||
---
|
||||
|
||||
### 2. Dynamic Type Support (HIGH - User Experience)
|
||||
|
||||
**Problem** Fixed font sizes prevent users with vision disabilities from reading text.
|
||||
|
||||
**WCAG** 1.4.4 Resize Text (Level AA - support 200% scaling without loss of content/functionality)
|
||||
|
||||
#### Common violations
|
||||
```swift
|
||||
// ❌ WRONG - Fixed size, won't scale
|
||||
Text("Price: $19.99")
|
||||
.font(.system(size: 17))
|
||||
|
||||
UILabel().font = UIFont.systemFont(ofSize: 17)
|
||||
|
||||
// ❌ WRONG - Custom font without scaling
|
||||
Text("Headline")
|
||||
.font(Font.custom("CustomFont", size: 24))
|
||||
|
||||
// ✅ CORRECT - SwiftUI semantic styles (auto-scales)
|
||||
Text("Price: $19.99")
|
||||
.font(.body)
|
||||
|
||||
Text("Headline")
|
||||
.font(.headline)
|
||||
|
||||
// ✅ CORRECT - UIKit semantic styles
|
||||
label.font = UIFont.preferredFont(forTextStyle: .body)
|
||||
|
||||
// ✅ CORRECT - Custom font with scaling
|
||||
let customFont = UIFont(name: "CustomFont", size: 24)!
|
||||
label.font = UIFontMetrics.default.scaledFont(for: customFont)
|
||||
label.adjustsFontForContentSizeCategory = true
|
||||
```
|
||||
|
||||
#### Custom sizes that scale with Dynamic Type
|
||||
```swift
|
||||
// ❌ WRONG - Fixed size, won't scale
|
||||
Text("Price: $19.99")
|
||||
.font(.system(size: 17))
|
||||
|
||||
// ⚠️ ACCEPTABLE - Custom font without scaling (accessibility violation)
|
||||
Text("Headline")
|
||||
.font(Font.custom("CustomFont", size: 24))
|
||||
|
||||
// ✅ GOOD - Custom size that scales with Dynamic Type
|
||||
Text("Large Title")
|
||||
.font(.system(size: 60).relativeTo(.largeTitle))
|
||||
|
||||
Text("Custom Headline")
|
||||
.font(.system(size: 24).relativeTo(.title2))
|
||||
|
||||
// ✅ BEST - Use semantic styles when possible
|
||||
Text("Headline")
|
||||
.font(.headline)
|
||||
```
|
||||
|
||||
**How `relativeTo:` works**
|
||||
- Base size: Your exact pixel size (24pt, 60pt, etc.)
|
||||
- Scales with: The text style you specify (`.title2`, `.largeTitle`, etc.)
|
||||
- Result: When user increases text size in Settings, your custom size grows proportionally
|
||||
|
||||
**Example**
|
||||
- `.title2` base: ~22pt → Your custom: 24pt (1.09x larger)
|
||||
- User increases to "Extra Large" text
|
||||
- `.title2` grows to ~28pt → Your custom grows to ~30.5pt (maintains 1.09x ratio)
|
||||
|
||||
**Fix hierarchy (best to worst)**
|
||||
1. **Best**: Use semantic styles (`.title`, `.body`, `.caption`)
|
||||
2. **Good**: Use `.system(size:).relativeTo()` for required custom sizes
|
||||
3. **Acceptable**: Custom font with `.dynamicTypeSize()` modifier
|
||||
4. **Unacceptable**: Fixed sizes that never scale
|
||||
|
||||
#### SwiftUI text styles
|
||||
- `.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
|
||||
|
||||
#### Layout considerations
|
||||
```swift
|
||||
// ❌ WRONG - Fixed frame breaks with large text
|
||||
Text("Long product description...")
|
||||
.font(.body)
|
||||
.frame(height: 50) // Clips at large text sizes
|
||||
|
||||
// ✅ CORRECT - Flexible frame
|
||||
Text("Long product description...")
|
||||
.font(.body)
|
||||
.lineLimit(nil) // Allow multiple lines
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
|
||||
// ✅ CORRECT - Stack rearranges at large sizes
|
||||
HStack {
|
||||
Text("Label:")
|
||||
Text("Value")
|
||||
}
|
||||
.dynamicTypeSize(...DynamicTypeSize.xxxLarge) // Limit maximum size if needed
|
||||
```
|
||||
|
||||
#### Testing
|
||||
1. Xcode Preview: Environment override
|
||||
```swift
|
||||
.environment(\.sizeCategory, .accessibilityExtraExtraExtraLarge)
|
||||
```
|
||||
|
||||
2. Simulator: Settings → Accessibility → Display & Text Size → Larger Text → Drag to maximum
|
||||
|
||||
3. Device: Settings → Accessibility → Display & Text Size → Larger Text
|
||||
|
||||
4. Check: Does text remain readable? Does layout adapt? Is any text clipped?
|
||||
|
||||
---
|
||||
|
||||
### 3. Color Contrast (HIGH - Vision Disabilities)
|
||||
|
||||
**Problem** Low contrast text is unreadable for users with vision disabilities or in bright sunlight.
|
||||
|
||||
#### WCAG
|
||||
- **1.4.3 Contrast (Minimum)** — Level AA
|
||||
- Normal text (< 18pt): 4.5:1 contrast ratio
|
||||
- Large text (≥ 18pt or ≥ 14pt bold): 3:1 contrast ratio
|
||||
- **1.4.6 Contrast (Enhanced)** — Level AAA
|
||||
- Normal text: 7:1 contrast ratio
|
||||
- Large text: 4.5:1 contrast ratio
|
||||
|
||||
#### Common violations
|
||||
```swift
|
||||
// ❌ WRONG - Low contrast (1.8:1 - fails WCAG)
|
||||
Text("Warning")
|
||||
.foregroundColor(.yellow) // on white background
|
||||
|
||||
// ❌ WRONG - Low contrast in dark mode
|
||||
Text("Info")
|
||||
.foregroundColor(.gray) // on black background
|
||||
|
||||
// ✅ CORRECT - High contrast (7:1+ passes AAA)
|
||||
Text("Warning")
|
||||
.foregroundColor(.orange) // or .red
|
||||
|
||||
// ✅ CORRECT - System colors adapt to light/dark mode
|
||||
Text("Info")
|
||||
.foregroundColor(.primary) // Black in light mode, white in dark
|
||||
|
||||
Text("Secondary")
|
||||
.foregroundColor(.secondary) // Automatic high contrast
|
||||
```
|
||||
|
||||
#### Differentiate Without Color
|
||||
```swift
|
||||
// ❌ WRONG - Color alone indicates status
|
||||
Circle()
|
||||
.fill(isAvailable ? .green : .red)
|
||||
|
||||
// ✅ CORRECT - Color + icon/text
|
||||
HStack {
|
||||
Image(systemName: isAvailable ? "checkmark.circle.fill" : "xmark.circle.fill")
|
||||
Text(isAvailable ? "Available" : "Unavailable")
|
||||
}
|
||||
.foregroundColor(isAvailable ? .green : .red)
|
||||
|
||||
// ✅ CORRECT - Respect system preference
|
||||
if UIAccessibility.shouldDifferentiateWithoutColor {
|
||||
// Use patterns, icons, or text instead of color alone
|
||||
}
|
||||
```
|
||||
|
||||
#### Testing
|
||||
1. Use Color Contrast Analyzer tool (free download)
|
||||
2. Screenshot your UI, measure text vs background
|
||||
3. Check both light and dark mode
|
||||
4. Settings → Accessibility → Display & Text Size → Increase Contrast (test with this ON)
|
||||
|
||||
#### Quick 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
|
||||
|
||||
---
|
||||
|
||||
### 4. Touch Target Sizes (MEDIUM - Motor Disabilities)
|
||||
|
||||
**Problem** Small tap targets are difficult or impossible for users with motor disabilities.
|
||||
|
||||
**WCAG** 2.5.5 Target Size (Level AAA - 44x44pt minimum)
|
||||
|
||||
**Apple HIG** 44x44pt minimum for all tappable elements
|
||||
|
||||
#### Common violations
|
||||
```swift
|
||||
// ❌ WRONG - Too small (24x24pt)
|
||||
Button("×") {
|
||||
dismiss()
|
||||
}
|
||||
.frame(width: 24, height: 24)
|
||||
|
||||
// ❌ WRONG - Small icon without padding
|
||||
Image(systemName: "heart")
|
||||
.font(.system(size: 16))
|
||||
.onTapGesture { }
|
||||
|
||||
// ✅ CORRECT - Minimum 44x44pt
|
||||
Button("×") {
|
||||
dismiss()
|
||||
}
|
||||
.frame(minWidth: 44, minHeight: 44)
|
||||
|
||||
// ✅ CORRECT - Larger icon or padding
|
||||
Image(systemName: "heart")
|
||||
.font(.system(size: 24))
|
||||
.frame(minWidth: 44, minHeight: 44)
|
||||
.contentShape(Rectangle()) // Expand tap area
|
||||
.onTapGesture { }
|
||||
|
||||
// ✅ CORRECT - UIKit button with edge insets
|
||||
button.contentEdgeInsets = UIEdgeInsets(top: 12, left: 12, bottom: 12, right: 12)
|
||||
// Total size: icon size + insets ≥ 44x44pt
|
||||
```
|
||||
|
||||
#### Spacing between targets
|
||||
```swift
|
||||
// ❌ WRONG - Targets too close (hard to tap accurately)
|
||||
HStack(spacing: 4) {
|
||||
Button("Edit") { }
|
||||
Button("Delete") { }
|
||||
}
|
||||
|
||||
// ✅ CORRECT - Adequate spacing (8pt minimum, 12pt better)
|
||||
HStack(spacing: 12) {
|
||||
Button("Edit") { }
|
||||
Button("Delete") { }
|
||||
}
|
||||
```
|
||||
|
||||
#### Testing
|
||||
1. Accessibility Inspector: Xcode → Open Developer Tool → Accessibility Inspector
|
||||
2. Select "Audit" tab → Run audit → Check for "Small Text" and "Hit Region" warnings
|
||||
3. Manual: Tap with one finger (not stylus) — can you hit it reliably without mistakes?
|
||||
|
||||
---
|
||||
|
||||
### 5. Keyboard Navigation (MEDIUM - iPadOS/macOS)
|
||||
|
||||
**Problem** Users who cannot use touch/mouse cannot navigate app.
|
||||
|
||||
**WCAG** 2.1.1 Keyboard (Level A - all functionality available via keyboard)
|
||||
|
||||
#### Common violations
|
||||
```swift
|
||||
// ❌ WRONG - Custom gesture without keyboard alternative
|
||||
.onTapGesture {
|
||||
showDetails()
|
||||
}
|
||||
// No way to trigger with keyboard
|
||||
|
||||
// ✅ CORRECT - Button provides keyboard support automatically
|
||||
Button("Show Details") {
|
||||
showDetails()
|
||||
}
|
||||
.keyboardShortcut("d", modifiers: .command) // Optional shortcut
|
||||
|
||||
// ✅ CORRECT - Custom control with focus support
|
||||
struct CustomButton: View {
|
||||
@FocusState private var isFocused: Bool
|
||||
|
||||
var body: some View {
|
||||
Text("Custom")
|
||||
.focusable()
|
||||
.focused($isFocused)
|
||||
.onKeyPress(.return) {
|
||||
action()
|
||||
return .handled
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Focus management
|
||||
```swift
|
||||
// ✅ CORRECT - Set initial focus
|
||||
.focusSection() // Group related controls
|
||||
.defaultFocus($focus, .constant(true)) // Set default
|
||||
|
||||
// ✅ CORRECT - Move focus after action
|
||||
@FocusState private var focusedField: Field?
|
||||
|
||||
Button("Next") {
|
||||
focusedField = .next
|
||||
}
|
||||
```
|
||||
|
||||
#### Testing (iPadOS/macOS)
|
||||
1. Connect keyboard to iPad or use Mac
|
||||
2. Press Tab - does focus move to interactive elements?
|
||||
3. Press Space/Return - does focused element activate?
|
||||
4. Check custom controls have visible focus indicator
|
||||
5. Can you reach all functionality without mouse/touch?
|
||||
|
||||
---
|
||||
|
||||
### 6. Reduce Motion Support (MEDIUM - Vestibular Disorders)
|
||||
|
||||
**Problem** Animations cause discomfort, nausea, or seizures for users with vestibular disorders.
|
||||
|
||||
**WCAG** 2.3.3 Animation from Interactions (Level AAA - motion animation can be disabled)
|
||||
|
||||
#### Common violations
|
||||
```swift
|
||||
// ❌ WRONG - Always animates (can cause nausea)
|
||||
.onAppear {
|
||||
withAnimation(.spring(response: 0.6, dampingFraction: 0.8)) {
|
||||
scale = 1.0
|
||||
}
|
||||
}
|
||||
|
||||
// ❌ WRONG - Parallax scrolling without opt-out
|
||||
ScrollView {
|
||||
GeometryReader { geo in
|
||||
Image("hero")
|
||||
.offset(y: geo.frame(in: .global).minY * 0.5) // Parallax
|
||||
}
|
||||
}
|
||||
|
||||
// ✅ CORRECT - Respect Reduce Motion preference
|
||||
.onAppear {
|
||||
if UIAccessibility.isReduceMotionEnabled {
|
||||
scale = 1.0 // Instant
|
||||
} else {
|
||||
withAnimation(.spring(response: 0.6, dampingFraction: 0.8)) {
|
||||
scale = 1.0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ✅ CORRECT - Simpler animation or cross-fade
|
||||
if UIAccessibility.isReduceMotionEnabled {
|
||||
// Cross-fade or instant change
|
||||
withAnimation(.linear(duration: 0.2)) {
|
||||
showView = true
|
||||
}
|
||||
} else {
|
||||
// Complex spring animation
|
||||
withAnimation(.spring()) {
|
||||
showView = true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### SwiftUI modifier
|
||||
```swift
|
||||
// ✅ CORRECT - Automatic support
|
||||
.animation(.spring(), value: isExpanded)
|
||||
.transaction { transaction in
|
||||
if UIAccessibility.isReduceMotionEnabled {
|
||||
transaction.animation = nil // Disable animation
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Testing
|
||||
1. Settings → Accessibility → Motion → Reduce Motion (toggle ON)
|
||||
2. Navigate app - are animations reduced or eliminated?
|
||||
3. Test: Transitions, scrolling effects, parallax, particle effects
|
||||
4. Video autoplay should also respect this preference
|
||||
|
||||
---
|
||||
|
||||
### 7. Common Violations (HIGH - App Store Review)
|
||||
|
||||
#### Images Without Labels
|
||||
|
||||
```swift
|
||||
// ❌ WRONG - Informative image without label
|
||||
Image("product-photo")
|
||||
|
||||
// ✅ CORRECT - Informative image with label
|
||||
Image("product-photo")
|
||||
.accessibilityLabel("Red sneakers with white laces")
|
||||
|
||||
// ✅ CORRECT - Decorative image hidden
|
||||
Image("background-pattern")
|
||||
.accessibilityHidden(true)
|
||||
```
|
||||
|
||||
#### Buttons With Wrong Traits
|
||||
|
||||
```swift
|
||||
// ❌ WRONG - Custom button without button trait
|
||||
Text("Submit")
|
||||
.onTapGesture {
|
||||
submit()
|
||||
}
|
||||
// VoiceOver announces as "Submit, text" not "Submit, button"
|
||||
|
||||
// ✅ CORRECT - Use Button for button-like controls
|
||||
Button("Submit") {
|
||||
submit()
|
||||
}
|
||||
// VoiceOver announces as "Submit, button"
|
||||
|
||||
// ✅ CORRECT - Custom control with correct trait
|
||||
Text("Submit")
|
||||
.accessibilityAddTraits(.isButton)
|
||||
.onTapGesture {
|
||||
submit()
|
||||
}
|
||||
```
|
||||
|
||||
#### Inaccessible Custom Controls
|
||||
|
||||
```swift
|
||||
// ❌ WRONG - Custom slider without accessibility support
|
||||
struct CustomSlider: View {
|
||||
@Binding var value: Double
|
||||
|
||||
var body: some View {
|
||||
// Drag gesture only, no VoiceOver support
|
||||
GeometryReader { geo in
|
||||
// ...
|
||||
}
|
||||
.gesture(DragGesture()...)
|
||||
}
|
||||
}
|
||||
|
||||
// ✅ CORRECT - Custom slider with accessibility actions
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Missing State Announcements
|
||||
|
||||
```swift
|
||||
// ❌ WRONG - State change without announcement
|
||||
Button("Toggle") {
|
||||
isOn.toggle()
|
||||
}
|
||||
|
||||
// ✅ CORRECT - State change with announcement
|
||||
Button("Toggle") {
|
||||
isOn.toggle()
|
||||
UIAccessibility.post(
|
||||
notification: .announcement,
|
||||
argument: isOn ? "Enabled" : "Disabled"
|
||||
)
|
||||
}
|
||||
|
||||
// ✅ CORRECT - Automatic state with accessibilityValue
|
||||
Button("Toggle") {
|
||||
isOn.toggle()
|
||||
}
|
||||
.accessibilityValue(isOn ? "Enabled" : "Disabled")
|
||||
```
|
||||
|
||||
## 8. Assistive Access Support (iOS 17+ — Cognitive Disabilities)
|
||||
|
||||
**Problem** App is unavailable or broken in Assistive Access mode, excluding users with cognitive disabilities who rely on a simplified system experience.
|
||||
|
||||
Assistive Access is a system-wide mode (Settings > Accessibility > Assistive Access) that replaces the standard iOS UI with large controls, simplified navigation, and reduced cognitive load. Apps that don't opt in are hidden from users in this mode.
|
||||
|
||||
#### Symptom: App missing from Assistive Access home screen
|
||||
|
||||
Your app doesn't appear under "Optimized Apps" in Assistive Access settings.
|
||||
|
||||
```xml
|
||||
<!-- ✅ FIX - Add to Info.plist -->
|
||||
<key>UISupportsAssistiveAccess</key>
|
||||
<true/>
|
||||
```
|
||||
|
||||
This makes the app available and launches it full screen in Assistive Access mode. Without this key, users in Assistive Access mode cannot access your app at all.
|
||||
|
||||
#### Symptom: Standard UI too complex for Assistive Access users
|
||||
|
||||
Your app launches in Assistive Access but shows the full standard interface, overwhelming users who need simplified controls.
|
||||
|
||||
```swift
|
||||
// ✅ FIX - Provide a dedicated Assistive Access scene
|
||||
@main
|
||||
struct MyApp: App {
|
||||
var body: some Scene {
|
||||
WindowGroup {
|
||||
ContentView() // Standard UI
|
||||
}
|
||||
|
||||
AssistiveAccess {
|
||||
AssistiveAccessContentView() // Simplified UI
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The `AssistiveAccess` scene type provides a separate entry point. When the system is in Assistive Access mode, it uses this scene instead of the standard `WindowGroup`. Native SwiftUI controls inside this scene automatically adopt the Assistive Access visual style (large buttons, prominent navigation, grid/row layout).
|
||||
|
||||
#### Symptom: App already designed for cognitive accessibility but displays in reduced frame
|
||||
|
||||
If your app is already purpose-built for users with cognitive disabilities (e.g., AAC apps), it may appear in a reduced frame rather than full screen.
|
||||
|
||||
```xml
|
||||
<!-- ✅ FIX - Add to Info.plist for apps already designed for cognitive accessibility -->
|
||||
<key>UISupportsFullScreenInAssistiveAccess</key>
|
||||
<true/>
|
||||
```
|
||||
|
||||
This displays your app identically to its standard appearance, bypassing the Assistive Access frame.
|
||||
|
||||
#### Detecting Assistive Access at runtime
|
||||
|
||||
```swift
|
||||
struct MyView: View {
|
||||
@Environment(\.accessibilityAssistiveAccessEnabled) var assistiveAccessEnabled
|
||||
|
||||
var body: some View {
|
||||
if assistiveAccessEnabled {
|
||||
// Simplified content
|
||||
} else {
|
||||
// Standard content
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### UIKit implementation
|
||||
|
||||
For UIKit apps, use the `.windowAssistiveAccessApplication` scene session role in your `UISceneConfiguration` to route to a dedicated scene delegate for the Assistive Access experience.
|
||||
|
||||
#### Design principles for Assistive Access scenes
|
||||
|
||||
- **Distill to core functionality** — One or two essential features, not the full app
|
||||
- **Large, prominent controls** — Ample spacing, no hidden gestures or timed interactions
|
||||
- **Multiple representations** — Pair text with icons; use visual alternatives
|
||||
- **Step-by-step navigation** — Clear back buttons, consistent patterns
|
||||
- **Safe interactions** — Remove irreversible actions; confirm destructive ones
|
||||
|
||||
#### Adding navigation icons
|
||||
|
||||
```swift
|
||||
NavigationStack {
|
||||
MyView()
|
||||
.navigationTitle("My Feature")
|
||||
.assistiveAccessNavigationIcon(systemImage: "star.fill")
|
||||
}
|
||||
```
|
||||
|
||||
#### Testing
|
||||
|
||||
1. **Device** — Enable Assistive Access in Settings > Accessibility > Assistive Access, verify app appears in "Optimized Apps", test the full user flow
|
||||
2. **Accessibility Inspector** — Run audit on the Assistive Access scene for label, contrast, and hit region issues
|
||||
|
||||
---
|
||||
|
||||
## Accessibility Inspector Workflow
|
||||
|
||||
### 1. Launch Accessibility Inspector
|
||||
|
||||
Xcode → Open Developer Tool → Accessibility Inspector
|
||||
|
||||
### 2. Select Target
|
||||
|
||||
- Dropdown: Choose running simulator or connected device
|
||||
- Target: Select your app
|
||||
|
||||
### 3. Inspection Mode
|
||||
|
||||
- Click "Inspection Pointer" button (crosshair icon)
|
||||
- Hover over UI elements to see:
|
||||
- Label, Value, Hint, Traits
|
||||
- Frame, Path
|
||||
- Actions available
|
||||
- Parent/child hierarchy
|
||||
|
||||
### 4. Run Audit
|
||||
|
||||
- Click "Audit" tab
|
||||
- Click "Run Audit" button
|
||||
- Review findings:
|
||||
- **Contrast** — Color contrast issues
|
||||
- **Hit Region** — Touch target size issues
|
||||
- **Clipped Text** — Text truncation with Dynamic Type
|
||||
- **Element Description** — Missing labels/hints
|
||||
- **Traits** — Wrong accessibility traits
|
||||
|
||||
### 5. Fix and Re-Test
|
||||
|
||||
- Click each finding for details
|
||||
- Fix in code
|
||||
- Re-run audit to verify
|
||||
|
||||
## VoiceOver Testing Checklist
|
||||
|
||||
### Enable VoiceOver
|
||||
- **Simulator** Cmd+F5 or Settings → Accessibility → VoiceOver
|
||||
- **Device** Triple-click side button (if enabled in Settings)
|
||||
|
||||
### Navigation Testing
|
||||
1. ☐ Swipe right/left - moves logically through UI elements
|
||||
2. ☐ Each element announces purpose clearly
|
||||
3. ☐ No unlabeled elements (except decorative)
|
||||
4. ☐ Heading navigation works (swipe up/down with 2 fingers)
|
||||
5. ☐ Container navigation works (swipe left/right with 3 fingers)
|
||||
|
||||
### Interaction Testing
|
||||
1. ☐ Double-tap activates buttons
|
||||
2. ☐ Swipe up/down adjusts sliders/pickers (with `.accessibilityAdjustableAction`)
|
||||
3. ☐ Custom gestures have VoiceOver equivalents
|
||||
4. ☐ Text fields announce keyboard type
|
||||
5. ☐ State changes are announced
|
||||
|
||||
### Content Testing
|
||||
1. ☐ Images have descriptive labels or are hidden
|
||||
2. ☐ Error messages are announced
|
||||
3. ☐ Loading states are announced
|
||||
4. ☐ Modal sheets announce role
|
||||
5. ☐ Alerts announce automatically
|
||||
|
||||
## App Store Review Preparation
|
||||
|
||||
### Required Accessibility Features (iOS)
|
||||
|
||||
1. **VoiceOver Support**
|
||||
- All UI elements must have labels
|
||||
- Navigation must be logical
|
||||
- All actions must be performable
|
||||
|
||||
2. **Dynamic Type**
|
||||
- Text must scale from -3 to +12 sizes
|
||||
- Layout must adapt without clipping
|
||||
|
||||
3. **Sufficient Contrast**
|
||||
- Minimum 4.5:1 for normal text
|
||||
- Minimum 3:1 for large text (≥18pt)
|
||||
|
||||
### App Store Connect Metadata
|
||||
|
||||
When submitting:
|
||||
1. Accessibility → Select features your app supports:
|
||||
- ☑ VoiceOver
|
||||
- ☑ Dynamic Type
|
||||
- ☑ Increased Contrast
|
||||
- ☑ Reduce Motion (if supported)
|
||||
|
||||
2. Test Notes: Document accessibility testing
|
||||
```
|
||||
Accessibility Testing Completed:
|
||||
- VoiceOver: All screens tested with VoiceOver enabled
|
||||
- Dynamic Type: Tested at all size categories
|
||||
- Color Contrast: Verified 4.5:1 minimum contrast
|
||||
- Touch Targets: All buttons minimum 44x44pt
|
||||
- Reduce Motion: Animations respect user preference
|
||||
```
|
||||
|
||||
### Common Rejection Reasons
|
||||
|
||||
1. **"App is not fully functional with VoiceOver"**
|
||||
- Missing labels on images/buttons
|
||||
- Unlabeled custom controls
|
||||
- Actions not performable with VoiceOver
|
||||
|
||||
2. **"Text is not readable at all Dynamic Type sizes"**
|
||||
- Fixed font sizes
|
||||
- Text clipping at large sizes
|
||||
- Layout breaks at accessibility sizes
|
||||
|
||||
3. **"Insufficient color contrast"**
|
||||
- Text fails 4.5:1 ratio
|
||||
- UI elements fail 3:1 ratio
|
||||
- Color-only indicators
|
||||
|
||||
---
|
||||
|
||||
## Design Review Pressure: Defending Accessibility Requirements
|
||||
|
||||
### The Problem
|
||||
|
||||
Under design review pressure, you'll face requests to:
|
||||
- "Those VoiceOver labels make the code messy - can we skip them?"
|
||||
- "Dynamic Type breaks our carefully designed layout - let's lock font sizes"
|
||||
- "The high contrast requirement ruins our brand aesthetic"
|
||||
- "44pt touch targets are too big - make them smaller for a cleaner look"
|
||||
|
||||
These sound like reasonable design preferences. **But they violate App Store requirements and exclude 15% of users.** Your job: defend using App Store guidelines and legal requirements, not opinion.
|
||||
|
||||
### Red Flags — Designer Requests That Violate Accessibility
|
||||
|
||||
If you hear ANY of these, **STOP and reference this skill**:
|
||||
|
||||
- ❌ **"Skip VoiceOver labels on icon-only buttons"** – App Store rejection (Guideline 2.5.1)
|
||||
- ❌ **"Use fixed 14pt font for compact design"** – Excludes users with vision disabilities
|
||||
- ❌ **"3:1 contrast ratio is fine"** – Fails WCAG AA for text (needs 4.5:1)
|
||||
- ❌ **"Make buttons 36x36pt for clean aesthetic"** – Fails touch target requirement (44x44pt minimum)
|
||||
- ❌ **"Disable Dynamic Type in this screen"** – App Store rejection risk
|
||||
- ❌ **"Color-code without labels (red=error, green=success)"** – Excludes colorblind users (8% of men)
|
||||
|
||||
### How to Push Back Professionally
|
||||
|
||||
#### Step 1: Show the Guideline
|
||||
|
||||
```
|
||||
"I want to support this design direction, but let me show you Apple's App Store
|
||||
Review Guideline 2.5.1:
|
||||
|
||||
'Apps should support accessibility features such as VoiceOver and Dynamic Type.
|
||||
Failure to include sufficient accessibility features may result in rejection.'
|
||||
|
||||
Here's what we need for approval:
|
||||
1. VoiceOver labels on all interactive elements
|
||||
2. Dynamic Type support (can't lock font sizes)
|
||||
3. 4.5:1 contrast ratio for text, 3:1 for UI
|
||||
4. 44x44pt minimum touch targets
|
||||
|
||||
Let me show where our design currently falls short..."
|
||||
```
|
||||
|
||||
#### Step 2: Demonstrate the Risk
|
||||
|
||||
Open the app with accessibility features enabled:
|
||||
- **VoiceOver** (Cmd+F5): Show buttons announcing "Button" instead of purpose
|
||||
- **Largest Text Size**: Show layout breaking or text clipping
|
||||
- **Color Contrast Analyzer**: Show failing contrast ratios
|
||||
- **Touch target overlay**: Show targets < 44pt
|
||||
|
||||
#### Reference
|
||||
- App Store Review Guideline 2.5.1
|
||||
- WCAG 2.1 Level AA (industry standard)
|
||||
- ADA compliance requirements (legal risk in US)
|
||||
|
||||
#### Step 3: Offer Compromise
|
||||
|
||||
```
|
||||
"I can achieve your aesthetic goals while meeting accessibility requirements:
|
||||
|
||||
1. VoiceOver labels: Add them programmatically (invisible in UI, required for approval)
|
||||
2. Dynamic Type: Use layout techniques that adapt (examples from Apple HIG)
|
||||
3. Contrast: Adjust colors slightly to meet 4.5:1 (I'll show options that preserve brand)
|
||||
4. Touch targets: Expand hit areas programmatically (visual size stays the same)
|
||||
|
||||
These changes won't affect the visual design you're seeing, but they're required
|
||||
for App Store approval and legal compliance."
|
||||
```
|
||||
|
||||
#### Step 4: Document the Decision
|
||||
|
||||
If overruled (designer insists on violations):
|
||||
|
||||
```
|
||||
Slack message to PM + designer:
|
||||
|
||||
"Design review decided to proceed with:
|
||||
- Fixed font sizes (disabling Dynamic Type)
|
||||
- 38x38pt buttons (below 44pt requirement)
|
||||
- 3.8:1 text contrast (below 4.5:1 requirement)
|
||||
|
||||
Important: These changes violate App Store Review Guideline 2.5.1 and WCAG AA.
|
||||
This creates three risks:
|
||||
|
||||
1. App Store rejection during review (adds 1-2 week delay)
|
||||
2. ADA compliance issues if user files complaint (legal risk)
|
||||
3. 15% of potential users unable to use app effectively
|
||||
|
||||
I'm flagging this proactively so we can prepare a response plan if rejected."
|
||||
```
|
||||
|
||||
#### Why this works
|
||||
- You're not questioning their design taste
|
||||
- You're raising App Store rejection risk (business impact)
|
||||
- You're citing specific guidelines (not opinion)
|
||||
- You're offering solutions that preserve visual design
|
||||
- You're documenting the decision (protects you post-rejection)
|
||||
|
||||
### Real-World Example: App Store Rejection (48-Hour Resubmit Window)
|
||||
|
||||
#### Scenario
|
||||
- 48 hours until resubmit deadline after rejection
|
||||
- Apple cited: "2.5.1 - Insufficient VoiceOver support"
|
||||
- Designer says: "Just add generic labels quickly"
|
||||
- PM watching the meeting, wants fastest fix
|
||||
|
||||
#### What to do
|
||||
|
||||
```swift
|
||||
// ❌ WRONG - Generic labels (will fail re-review)
|
||||
Button(action: addToCart) {
|
||||
Image(systemName: "cart.badge.plus")
|
||||
}
|
||||
.accessibilityLabel("Button") // Apple will reject again
|
||||
|
||||
// ✅ CORRECT - Descriptive labels (passes review)
|
||||
Button(action: addToCart) {
|
||||
Image(systemName: "cart.badge.plus")
|
||||
}
|
||||
.accessibilityLabel("Add to cart")
|
||||
.accessibilityHint("Double-tap to add this item to your shopping cart")
|
||||
```
|
||||
|
||||
#### In the meeting, demonstrate
|
||||
1. Enable VoiceOver (Cmd+F5)
|
||||
2. Show "Button" announcement (generic - fails)
|
||||
3. Show "Add to cart" announcement (descriptive - passes)
|
||||
4. Reference Apple's rejection message: "Elements must have descriptive labels"
|
||||
|
||||
**Time estimate** 2-4 hours to audit all interactive elements and add proper labels.
|
||||
|
||||
#### Result
|
||||
- Honest time estimate prevents second rejection
|
||||
- Proper labels pass Apple review
|
||||
- Resubmit accepted within 48 hours
|
||||
|
||||
### When to Accept the Design Decision (Even If You Disagree)
|
||||
|
||||
Sometimes designers have valid reasons to override accessibility guidelines. Accept if:
|
||||
|
||||
- [ ] They understand the App Store rejection risk
|
||||
- [ ] They're willing to delay launch if rejected
|
||||
- [ ] You document the decision in writing
|
||||
- [ ] They commit to fixing if rejected
|
||||
|
||||
#### Document in Slack
|
||||
|
||||
```
|
||||
"Design review decided to proceed with [specific violations].
|
||||
|
||||
We understand this creates:
|
||||
- App Store rejection risk (Guideline 2.5.1)
|
||||
- Potential 1-2 week delay if rejected
|
||||
- Need to audit and fix all instances if rejected
|
||||
|
||||
Monitoring plan:
|
||||
- Submit for review with current design
|
||||
- If rejected, implement proper accessibility (estimated 2-4 hours)
|
||||
- Have accessibility-compliant version ready as backup"
|
||||
```
|
||||
|
||||
This protects both of you and shows you're not blocking - just de-risking.
|
||||
|
||||
---
|
||||
|
||||
## WCAG Compliance Levels
|
||||
|
||||
### Level A (Minimum — Required for App Store)
|
||||
- 1.1.1 Non-text Content — Images have text alternatives
|
||||
- 2.1.1 Keyboard — All functionality via keyboard (iPadOS/macOS)
|
||||
- 4.1.2 Name, Role, Value — Elements have accessible names
|
||||
|
||||
### Level AA (Standard — Recommended)
|
||||
- 1.4.3 Contrast (Minimum) — 4.5:1 text, 3:1 UI
|
||||
- 1.4.4 Resize Text — Support 200% text scaling
|
||||
- 1.4.5 Images of Text — Use real text when possible
|
||||
|
||||
### Level AAA (Enhanced — Best Practice)
|
||||
- 1.4.6 Contrast (Enhanced) — 7:1 text, 4.5:1 UI
|
||||
- 2.3.3 Animation from Interactions — Reduce Motion support
|
||||
- 2.5.5 Target Size - 44x44pt minimum targets
|
||||
|
||||
**Goal** Meet Level AA for all content, Level AAA where feasible.
|
||||
|
||||
## Quick Command Reference
|
||||
|
||||
After making fixes:
|
||||
|
||||
```bash
|
||||
# Quick scan for new issues
|
||||
/axiom:audit-accessibility
|
||||
|
||||
# Deep diagnosis for specific issues
|
||||
/skill axiom:accessibility-diag
|
||||
```
|
||||
|
||||
## Resources
|
||||
|
||||
**Docs**: /accessibility/voiceover, /uikit/uifont/scaling_fonts_automatically
|
||||
|
||||
---
|
||||
|
||||
**Remember** Accessibility is not a feature, it's a requirement. 15% of users have some form of disability. Making your app accessible isn't just the right thing to do - it expands your user base and improves the experience for everyone.
|
||||
@@ -0,0 +1,3 @@
|
||||
interface:
|
||||
display_name: "Accessibility Diagnostics"
|
||||
short_description: "Fixing VoiceOver issues, Dynamic Type violations, color contrast failures, touch target problems, keyboard navigation..."
|
||||
Reference in New Issue
Block a user