ios-accessibility — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited ios-accessibility (Agent Skill) and scored it 45/100 (orange). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 1 high-severity and 1 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 2 flagged
A base64 string of 128+ characters appears in a documentation file. Encoded prompt injection hides the hostile instruction in base64 — invisible to keyword filters — and relies on the agent's ability to decode it at runtime. There is no normal authoring reason to embed a multi-hundred-byte base64 blob in skill docs.
*.sig, SIGNATURES) outside the documentation.The text {match} tells the agent to skip the normal "ask the user first" gate. Used adversarially it removes the human-in-the-loop check before destructive or sensitive actions, turning a normally-gated agent into a fire-and-forget executor.
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
Production-grade accessibility skill for iOS codebases aligned with WCAG 2.2 AA and Apple Human Interface Guidelines. Operates correction-first — AI coding assistants (including Claude) systematically generate inaccessible iOS code because most training data lacks accessibility. This skill intercepts those patterns and enforces accessible output from the start.
The skill covers both SwiftUI and UIKit with framework-appropriate patterns, and produces code that works across VoiceOver, Switch Control, Voice Control, Full Keyboard Access, and Dynamic Type.
Research (CodeA11y, arXiv 2502.10884) found that AI omits accessibility because: (1) developers don't prompt for it so AI doesn't provide it, (2) most training code is inaccessible, and (3) generated code omits manual steps like replacing placeholder labels. This skill ensures every code generation pass includes accessibility from the start — not as an afterthought.
Is the file SwiftUI (.swift with View conformance)?
├── YES → Apply SwiftUI patterns → Read references/swiftui-patterns.md
└── NO → Is the file UIKit (.swift with UIView/UIViewController)?
├── YES → Apply UIKit patterns → Read references/uikit-patterns.md
└── NO → Mixed — apply BOTH checklists per file typeCan the user tap/activate this element?
├── YES → Is it using Button, Toggle, Slider, Picker, or Link?
│ ├── YES → Standard control — verify accessibilityLabel exists
│ └── NO → Using onTapGesture or custom view?
│ └── 🔴 REPLACE with Button or appropriate standard control
│ onTapGesture is invisible to VoiceOver, Switch Control, and eye tracking
└── NO → Is it a meaningful image or icon?
├── YES → Add accessibilityLabel describing content, not appearance
└── NO → Decorative → Image(decorative:) or .accessibilityHidden(true)Is the font set with .font(.system(size: N))?
├── YES → 🔴 REPLACE with Dynamic Type style (.title, .body, .caption, etc.)
└── NO → Is it a custom font?
├── YES → SwiftUI: .custom("Name", size: N, relativeTo: .body)
│ UIKit: UIFontMetrics(forTextStyle:).scaledFont(for:)
└── NO → Using text style (.body, .headline, etc.)? → OKIs the container a single conceptual item? (e.g., product card)
├── YES → Are children's labels sufficient when joined?
│ ├── YES → .accessibilityElement(children: .combine)
│ └── NO → .accessibilityElement(children: .ignore) + custom label
└── NO → Are children independently interactive?
├── YES → .accessibilityElement(children: .contain) or default
└── NO → .combine or .ignore based on reading qualityDoes the issue make content invisible to assistive technology?
├── YES → 🔴 CRITICAL (onTapGesture, missing labels on buttons, hidden disabled controls)
└── NO → Does it break under system accessibility settings?
├── YES → 🟡 HIGH (hardcoded fonts, hardcoded colors, ignoring reduce motion)
└── NO → Is it a defense-in-depth gap?
├── YES → 🟢 MEDIUM (missing hints, no custom actions on list cells, no rotors)
└── NO → 🔵 LOW (best practice: input labels for Voice Control, custom content)onTapGesture for interactive elements, missing accessibilityLabel on icon buttons, isAccessibilityElement = false on disabled controls, traits assigned instead of inserted (UIKit)..system(size:)), hardcoded colors (.black, .white), deprecated API (foregroundColor, NavigationView, cornerRadius), ignoring reduceMotion, no numberOfLines = 0 on UIKit labels.accessibilityHint, no custom actions on list cells, missing .isHeader traits on section headings, no grouping on related elements, no accessibilityValue on stateful controls.accessibilityInputLabels for Voice Control, no accessibilityCustomContent for secondary info, no Large Content Viewer on toolbar items.For complete patterns with all 11 failure modes and code pairs, read references/ai-failure-patterns.md| # | AI Failure | Search for | Severity |
|---|---|---|---|
| F1 | onTapGesture instead of Button | .onTapGesture on interactive elements | 🔴 |
| F2 | Hardcoded font sizes | .font(.system(size: | 🟡 |
| F3 | Hardcoded colors for text/bg | .foregroundColor(.black), .background(Color.white) | 🟡 |
| F4 | Deprecated API | .foregroundColor(, .cornerRadius(, NavigationView | 🟡 |
| F5 | No labels on image buttons | Image(systemName: inside Button without .accessibilityLabel | 🔴 |
| F6 | GeometryReader abuse + fixed frames | GeometryReader, .frame(width: with hardcoded values | 🟡 |
| F7 | No accessibility on custom controls | Custom views without any accessibility modifiers | 🔴 |
| F8 | accessibilityIdentifier vs accessibilityLabel confusion | .accessibilityIdentifier used where .accessibilityLabel needed | 🔴 |
| F9 | Missing system preference checks | No @Environment for reduceMotion, dynamicTypeSize, etc. | 🟡 |
| F10 | Assigning traits instead of inserting (UIKit) | .accessibilityTraits = .selected (destroys existing traits) | 🔴 |
| F11 | Hiding disabled controls | isAccessibilityElement = false on disabled buttons | 🔴 |
When: User asks to "review accessibility", "check VoiceOver support", "audit a11y", or any variant.
references/ai-failure-patterns.md — scan for all 11 AI failure patternsreferences/motion-input.mdreferences/wcag-ios-mapping.mdreferences/compliance.mdWhen: Creating a new SwiftUI or UIKit screen. Apply from the start so no retrofitting is needed.
references/ai-failure-patterns.md — internalize patterns to avoidButton for all interactive elements (never onTapGesture).system(size:)).primary, .secondary, Color(.systemBackground)).accessibilityLabel to every image button and custom control.accessibilityAddTraits(.isHeader) to section headings.accessibilityElement(children: .combine)@Environment(\.accessibilityReduceMotion) for any animations.accessibilityValue to stateful controls (toggles, sliders, custom)When: User asks "how do I fix this VoiceOver issue" or "make this control accessible"
When: User asks "is this ready for release" or "accessibility checklist"
references/testing.md — verify XCTest accessibility audit is configuredreferences/wcag-ios-mapping.md — check WCAG 2.2 AA coveragereferences/compliance.md for legal requirementsWhen: User asks about Dynamic Type, text scaling, or font accessibility
references/dynamic-type.md — full Dynamic Type patterns.font(.system(size: — replace with text stylesadjustsFontForContentSizeCategory = trueScrollView wraps content that may overflow@ScaledMetric usage for non-text dimensions (icons, spacing)Use this table when asked which WCAG criteria apply, what's auto-handled, and what needs manual work. Native iOS platform behavior auto-satisfies many criteria, and some criteria simply do not apply to native iOS (they exist for HTML/web).
| Criterion | Why auto |
|---|---|
| 1.3.4 Orientation | iOS rotates automatically unless the app locks it |
| 1.4.4 Resize Text | Dynamic Type handles this when the app uses text styles |
| 2.1.1 Keyboard | Full Keyboard Access navigates standard controls |
| 2.4.7 Focus Visible | UIKit/SwiftUI show a focus ring in Full Keyboard Access |
| 1.4.10 Reflow | Auto Layout + Dynamic Type reflow by default |
| Criterion | Manual action |
|---|---|
| 1.1.1 Non-text Content | Every meaningful image/icon needs accessibilityLabel; decorative images need Image(decorative:) or .accessibilityHidden(true) |
| 1.3.1 Info and Relationships | Heading traits (.isHeader), grouping (.accessibilityElement(children:)), table/form structure |
| 1.4.3 Contrast (AA) | 4.5:1 for body text, 3:1 for large text — test in BOTH light and dark modes independently |
| 1.4.6 Contrast Enhanced (AAA) | 7:1 body text — required when user enables Increase Contrast system setting |
| 2.5.7 Dragging Movements (WCAG 2.2 NEW) | Provide single-tap alternative via accessibilityCustomActions for any drag operation |
| 2.5.8 Target Size Minimum (WCAG 2.2 NEW) | 24×24pt minimum (AA), 44×44pt preferred (Apple HIG) |
| 3.3.7 Redundant Entry (WCAG 2.2 NEW) | Auto-fill or remember previously-entered info in multi-step forms |
| 3.3.8 Accessible Authentication (WCAG 2.2 NEW) | No cognitive puzzles for login; support paste, password managers, biometrics |
These are web/HTML-specific — calling them out is important because AI often copies them from web checklists:
accessibilityLabel/traits/value in native code, not markup.When asked "which WCAG criteria apply to iOS", always explicitly name 1.1.1 Non-text Content as manual (it's the most-missed), identify at least one of the four NEW 2.2 criteria (2.5.7, 2.5.8, 3.3.7, 3.3.8), and note that 4.1.1 and 2.4.1 don't apply.
These APIs frequently come up in reviews but are easy to forget. Surfaced here from the references so the body of a response can cite them directly.
Text("Email:")
.accessibilityLabeledPair(role: .label, id: "emailField", in: namespace)
TextField("", text: $email)
.accessibilityLabeledPair(role: .content, id: "emailField", in: namespace)Use .accessibilityLabeledPair(role:id:in:) (iOS 17+) to tell VoiceOver that a separate label view describes a nearby control. Without it, VoiceOver reads them as independent elements and the user doesn't know the label belongs to the field.
modalView.accessibilityViewIsModal = trueTwo requirements to make this work:
accessibilityViewIsModal only hides the siblings of the modal view — not all other views in the hierarchy. The modal must be a direct child of the container whose siblings you want hidden (typically the window or a top-level container).accessibilityPerformEscape on the modal so the VoiceOver user can dismiss it with a two-finger Z-gesture: override func accessibilityPerformEscape() -> Bool { dismiss(...); return true }When a single UIView draws its own hit-testable sub-regions (like bars in a bar chart), expose each region as a separate UIAccessibilityElement:
override var accessibilityElements: [Any]? {
bars.map { bar in
let element = UIAccessibilityElement(accessibilityContainer: self)
element.accessibilityLabel = "\(bar.label), \(bar.value)"
element.accessibilityTraits = .staticText
// Use SCREEN coordinates for custom-drawn elements:
element.accessibilityFrame = UIAccessibility.convertToScreenCoordinates(bar.frame, in: self)
return element
}
}Set isAccessibilityElement = false on the parent container so VoiceOver sees the children. Use UIAccessibility.convertToScreenCoordinates(_:in:) for the frame — this is the canonical API for custom-drawn views because the system expects screen-space coordinates for hit-testing the VoiceOver cursor. (accessibilityFrameInContainerSpace is an alternative but requires the view to be in a standard container hierarchy, which custom-drawn graphs often aren't.)
cell.shouldGroupAccessibilityChildren = true // group children into one swipe stop
titleLabel.accessibilitySortPriority = 2 // read first (higher = earlier)
subtitleLabel.accessibilitySortPriority = 1 // read secondshouldGroupAccessibilityChildren keeps VoiceOver focused on the cell (prevents wandering into children) while accessibilitySortPriority controls reading order within the group.
The gold-standard VoiceOver manual test: enable VoiceOver, then triple-tap with three fingers to toggle Screen Curtain — the display goes black while VoiceOver continues. Navigate the screen this way. If you can't complete the task, there's an accessibility gap. Every release checklist should include a Screen Curtain pass for critical flows.
### [SEVERITY] [Short title]
**File:** `path/to/file.swift:42`
**WCAG:** [Criterion if applicable] | **HIG:** [Guideline if applicable]
**Issue:** [1-2 sentence description]
**VoiceOver Impact:** [What the user hears/doesn't hear]
**Fix:**// ❌ Current [problematic code]
// ✅ Corrected [accessible replacement]
<critical_rules>
Whether generating new code or reviewing existing code, ALWAYS enforce these rules. They correct the systematic accessibility failures that AI coding assistants produce:
onTapGesture for interactive elements — always use Button. Views with onTapGesture are invisible to VoiceOver, Switch Control, and visionOS eye tracking. This is the #1 AI failure..font(.system(size: N)) — always use Dynamic Type text styles (.title, .body, .caption, etc.). Hardcoded sizes break for 25%+ of users who change text size..foregroundColor(.black) or .background(Color.white) — use .foregroundStyle(.primary) and Color(.systemBackground). Hardcoded colors are invisible in dark mode..accessibilityLabel to image-only buttons. Image(systemName: "plus") inside a Button reads as the raw SF Symbol name without a label..accessibilityAddTraits(.isHeader) on section headings — enables VoiceOver rotor heading navigation..accessibilityElement(children: .combine) or .ignore + custom label to group related content (product cards, list cells, table rows)..insert(.selected) — never .accessibilityTraits = .selected which DESTROYS existing traits like .isButton..disabled(true) (SwiftUI auto-adds .notEnabled trait) or .insert(.notEnabled) in UIKit. VoiceOver reads "dimmed" so the user knows the control exists..foregroundStyle() not .foregroundColor(), .clipShape(.rect(cornerRadius:)) not .cornerRadius(), NavigationStack not NavigationView.@Environment(\.accessibilityReduceMotion) — replace motion with crossfade/opacity when enabled.accessibilityLabel — say "Play" not "Play button" (VoiceOver adds "button" from the trait).Image(decorative:) for decorative images — not Image("bg").accessibilityHidden(true). When images inside a grouped container are purely visual (e.g., star icons in a rating display), hide each individual image with .accessibilityHidden(true) and provide a single meaningful description on the parent..accessibilityAddTraits(.isToggle) (iOS 17+) on any custom toggle-like control — VoiceOver announces "Toggle" so users know the control switches between states. Group the toggle's label and visual indicator with .accessibilityElement(children: .combine) or .ignore.@ScaledMetric(relativeTo:) for non-text dimensions — including image sizes, icons, spacing, and avatars — so they scale proportionally with Dynamic Type: @ScaledMetric(relativeTo: .body) var iconSize: CGFloat = 24. Wrap content in ScrollView for overflow at accessibility text sizes. For UIKit labels always set numberOfLines = 0 so scaled text can wrap. Toolbar/tab bar items should cap at .xxxLarge via .dynamicTypeSize(...DynamicTypeSize.xxxLarge) — unbounded scaling breaks toolbar layouts, and iOS shows a Large Content Viewer for bigger sizes instead.@Environment key AND, where applicable, a SwiftUI/UIKit opt-out modifier:reduceMotion — replace motion with crossfade/opacityreduceTransparency — use opaque backgrounds instead of .ultraThinMateriallegibilityWeight (Bold Text) — honor via Font.weight(legibilityWeight == .bold ? .bold : .regular)colorSchemeContrast — when .increased, contrast ratio targets rise from 4.5:1 (AA) to 7:1 (AAA) for body text; test contrast in BOTH light and dark modes independentlyaccessibilityDifferentiateWithoutColor — never convey status with color alone; add a shape, icon, or patterninvertColors — photos, videos, and correctly-colored icons should opt out via .accessibilityIgnoresInvertColors(true) (SwiftUI) or accessibilityIgnoresInvertColors = true (UIKit). This property does not cascade to child views — apply it on each leaf view that renders photo content.dynamicTypeSize — use @Environment(\.dynamicTypeSize) to switch HStack → VStack at .accessibility1 and aboveaccessibilityCustomActions, and ensure touch target size is at least 24×24pt (WCAG 2.5.8 AA minimum) or ideally 44×44pt (Apple HIG). When answering drag-and-drop accessibility questions, cite WCAG 2.5.7 specifically — it is the criterion that directly applies.</critical_rules>
<fallback_strategies>
If unsure whether an image is decorative or informative: Ask the user. Rule of thumb: if removing the image changes the meaning of the screen, it's informative and needs a label. If it's purely aesthetic, it's decorative.
If a custom control is too complex for standard accessibility modifiers: Use .accessibilityRepresentation to provide an alternative accessible view (e.g., a custom gauge represented as a Slider for VoiceOver).
If VoiceOver reading order is wrong after layout changes: In SwiftUI, use .accessibilitySortPriority() (higher = read earlier). In UIKit, override accessibilityElements array to define explicit order.
If modal focus trapping isn't working: In UIKit, verify accessibilityViewIsModal = true is on the modal AND the modal is a direct child of the container whose siblings you want hidden — this property only hides SIBLINGS, not all other views. Also implement accessibilityPerformEscape on the modal so VoiceOver users can dismiss it with the two-finger Z-gesture. See "Specific APIs Quick Reference → Modal focus trapping" above.
If Dynamic Type breaks layout at accessibility sizes: Use ViewThatFits (iOS 16+) to automatically switch between horizontal and vertical layouts. Wrap content in ScrollView for overflow. Use AnyLayout to preserve state during layout changes.
If grouping with `.combine` produces awkward VoiceOver reading: Switch to .ignore + custom accessibilityLabel with a natural sentence. .combine joins labels with pauses; .ignore lets you write a coherent sentence. </fallback_strategies>
Before finalizing generated or reviewed code, verify ALL:
[ ] No onTapGesture on interactive elements — all use Button or standard controls
[ ] No hardcoded font sizes — all text uses Dynamic Type styles
[ ] No hardcoded colors for text/backgrounds — all use semantic colors
[ ] Every image-only button has accessibilityLabel
[ ] Section headings have .accessibilityAddTraits(.isHeader)
[ ] Related content grouped with .accessibilityElement(children:)
[ ] List cells with multiple actions use accessibilityCustomActions
[ ] Animations check @Environment(\.accessibilityReduceMotion)
[ ] Disabled controls visible to VoiceOver (not hidden, using .notEnabled)
[ ] UIKit traits inserted (.insert) not assigned (= .trait)
[ ] Labels describe purpose, not appearance ("Add to favorites" not "Heart icon")
[ ] Labels don't include element type ("Play" not "Play button")
[ ] Decorative images hidden from VoiceOver
[ ] Custom controls have label, value, traits, and adjustable action where appropriate
[ ] Custom toggles use .accessibilityAddTraits(.isToggle) and .accessibilityValue("On"/"Off")
[ ] Non-text dimensions (icons, spacing) use @ScaledMetric for Dynamic Type scaling
[ ] Content wrapped in ScrollView for overflow at accessibility text sizes
[ ] All system preferences checked: reduceMotion, reduceTransparency, legibilityWeight, colorSchemeContrast, differentiateWithoutColor, invertColors
[ ] Touch targets meet 44×44pt minimum (Apple HIG) / 24×24pt (WCAG 2.5.8)
[ ] Drag operations have single-tap alternatives via accessibilityCustomActions
[ ] Modern API used (foregroundStyle, clipShape, NavigationStack)| Finding type | Companion skill | Apply when |
|---|---|---|
| Architecture patterns affecting accessibility | skills/swiftui-mvvm/SKILL.md | Structuring ViewModels that manage accessibility state |
| Security + accessibility overlap (biometric auth) | skills/ios-security/SKILL.md | LAContext with proper VoiceOver feedback |
| Testing accessibility in CI | skills/ios-testing/SKILL.md | XCTest accessibility audits, snapshot tests |
| Concurrency in VoiceOver announcements | skills/swift-concurrency/SKILL.md | Posting notifications from async contexts |
| Reference | When to Read |
|---|---|
references/rules.md | Do's and Don'ts quick reference: priority rules and critical anti-patterns |
references/ai-failure-patterns.md | Every code generation/review — all 11 AI failure patterns with ❌/✅ code pairs |
references/voiceover-patterns.md | VoiceOver work — labels, hints, traits, grouping, custom actions, rotors, navigation |
references/swiftui-patterns.md | SwiftUI accessibility — all modifiers, component patterns, focus management |
references/uikit-patterns.md | UIKit accessibility — elements, containers, traits, notifications, modal views |
references/dynamic-type.md | Dynamic Type — UIFontMetrics, @ScaledMetric, layout adaptation, Large Content Viewer |
references/color-visual.md | Color/contrast — WCAG ratios, color blindness, dark mode, reduce transparency, smart invert |
references/motion-input.md | Motion & input — reduce motion, switch control, voice control, full keyboard access |
references/wcag-ios-mapping.md | WCAG compliance — 2.2 AA criteria mapped to iOS APIs, what's auto-handled vs manual |
references/testing.md | Testing — Xcode Accessibility Inspector, XCTest audits, VoiceOver manual protocol, CI setup |
references/ios-new-features.md | iOS 17/18/26 — new accessibility APIs, modifiers, and platform features |
references/compliance.md | Regulated apps — ADA, EAA, Section 508, VPAT, documentation requirements |
~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.