
Ios Accessibility
- 643 installs
- 165 repo stars
- Updated March 9, 2026
- dadederk/ios-accessibility-agent-skill
ios-accessibility is an agent skill that provides expert iOS guidance on VoiceOver, Dynamic Type, accessibility labels, traits, and assistive-technology testing for developers building inclusive UIKit and SwiftUI applica
About
ios-accessibility is the dadederk agent skill for implementing Apple platform accessibility correctly the first time. It covers VoiceOver navigation, Dynamic Type scaling, accessibility labels/traits/hints/values, Switch Control, Voice Control, Full Keyboard Access, and inclusive design culture practices referenced in its trigger list. The skill supports both manual testing walkthroughs and automated accessibility auditing workflows while building UIKit or SwiftUI screens. Developers invoke it when App Store accessibility failures, audit warnings, or user reports reveal missing labels or broken rotor navigation. It bridges HIG requirements and code-level APIs so agents output production-ready accessible components rather than generic web a11y advice misapplied to iOS.
- Expert guidance on VoiceOver, Dynamic Type, assistive technologies and inclusive design
- Covers both UIKit and SwiftUI accessibility implementations
- Shift-left approach integrating accessibility from prototypes through production
- Recommends testing as you go with manual and automated methods
- Presents solutions with confidence-ordered pros, cons and trade-offs
Ios Accessibility by the numbers
- 643 all-time installs (skills.sh)
- Ranked #520 of 1,880 Design & UI/UX skills by installs in the Skillselion catalog
- Data as of Jul 30, 2026 (Skillselion catalog sync)
npx skills add https://github.com/dadederk/ios-accessibility-agent-skill --skill ios-accessibilityAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 643 |
|---|---|
| repo stars | ★ 165 |
| Last updated | March 9, 2026 |
| Repository | dadederk/ios-accessibility-agent-skill ↗ |
How do you implement VoiceOver accessibility in SwiftUI?
Get expert iOS-specific guidance on VoiceOver, Dynamic Type, accessibility labels, traits, and inclusive implementation patterns while building mobile apps.
Who is it for?
iOS developers shipping UIKit or SwiftUI features who need VoiceOver, Dynamic Type, and assistive-tech patterns beyond generic web accessibility guidance.
Skip if: Android Compose accessibility work, backend-only services, or teams skipping mobile assistive-technology requirements.
When should I use this skill?
User mentions iOS accessibility, VoiceOver, Dynamic Type, accessibility labels, traits, Switch Control, or inclusive iOS design.
What you get
Accessible iOS UI with VoiceOver labels, traits, Dynamic Type support, and documented manual or automated accessibility test steps.
- Accessible view implementations
- Accessibility audit checklist results
- VoiceOver navigation documentation
Files
iOS Accessibility
Overview
This skill provides expert guidance on iOS accessibility, covering VoiceOver, Dynamic Type, assistive technologies, inclusive design practices, and both UIKit and SwiftUI implementations. Use this skill to help developers build apps that work for everyone.
The Approach
- Shift-left — Accessibility is part of the process. It needs to be considered even in prototypes or MVPs.
- User-centric — Accessibility is about people. Checklists help, but the goal is not checklist compliance. The goal is to offer a great experience for users with disabilities.
- Progress over perfection — Anytime is a good time to start. Focus on iterative and incremental improvements as you go. It goes a long way.
- Test as you go — Manual testing is part of development.
Agent Behavior Contract
1. Accessibility is non-deterministic. Propose potential solutions in order of confidence and present with clear pros, cons, and trade-offs. 2. Before proposing fixes, identify the platform (UIKit vs SwiftUI) and the assistive technology, accessibility feature, or design consideration in context. 3. Do not recommend accessibility fixes without considering the user experience impact. 4. Prefer manual testing guidance alongside code changes, together with any automated or semi-automated solutions. 5. Cross-reference multiple assistive technologies when relevant (VoiceOver, Voice Control, Switch Control, Full Keyboard Access).
Anti-Patterns to Avoid
- Do not add trait names to labels — Say "Close", not "Close button" (VoiceOver adds "button" automatically, when using the button trait)
- Do not use `.accessibilityHidden(true)` on interactive elements — Users won't be able to access them
- Do not use fixed font sizes — Always use text styles for Dynamic Type support
- Do not use hardcoded colors for text — Use semantic colors (
.label,.secondaryLabel) for contrast and Dark Mode - Do not group UIKit elements without a clear combined label — If
isAccessibilityElement = true, setaccessibilityLabel(and value/traits as needed). - Do not group SwiftUI elements without a clear combined label — If
.accessibilityElement(children: .ignore)is used, provide label/value/traits manually. - Do not add hints unless needed — It should be clear what a component expresses, and does, by its label/value/traits. Only configure for adding extra clarity or context.
- Do not rely on `onTapGesture` alone — Prefer semantic controls like
Button. If gesture handling is unavoidable, add button traits and clear labels. - Do not scale chrome controls with Dynamic Type — For navigation bars, toolbars, and tab bars, prefer Large Content Viewer (iOS 13+) using `.accessibilityShowsLargeContentViewer`) / `UILargeContentViewerItem`.
General Guidance
Prefer native components: Whenever possible, use Apple's native components and customize them to your needs instead of building custom components from scratch.
Design system first: Whenever the project uses a design system of its own (colors, text styles, component catalog), propose changes in the design system itself so the improvement snowballs everywhere in the app using an improved component.
Platform parity: The same accessibility principles apply to both UIKit and SwiftUI, but APIs and implementation details differ.
Project Settings Intake (Evaluate Before Advising)
Before providing accessibility guidance, determine:
Project Capabilities
- Is the project using SwiftUI, UIKit, or a mix of both?
- iOS deployment target — Some APIs require specific versions:
- iOS 13+: Large Content Viewer (`UILargeContentViewerInteraction`), SF Symbols
- iOS 14+: Switch Control action images (`UIAccessibilityCustomAction.init(name:image:actionHandler:)`))
- iOS 15+: `AccessibilityFocusState`, `.accessibilityRotor`)
- iOS 16+: `.accessibilityRepresentation`), `.accessibilityActions { }` syntax)
- iOS 17+: `.sensoryFeedback`)
- Check minimum OS — Look for
#availablechecks and deployment target in project settings
Project Conventions
- Design system — Does the project define its own design system (colors, text styles, UI component catalogue)? Propose changes in the design system when appropriate, not only per-feature.
- Semantic colors and text styles — Does the project use semantic colors (
.label,.systemBackground) and text styles (.preferredFont(forTextStyle:)in UIKit,.font(.body)in SwiftUI) vs hardcoded values? - Existing accessibility patterns — Search for
.accessibilityLabel,.accessibilityTraits, etc. to match project style. - Localization — Accessibility labels, values, and hints should be localized. Match the project's localization conventions.
- UI construction — Interface Builder (XIB/Storyboard) or code-only?
- Custom gestures — Identify if custom gestures need accessible alternatives.
- Accessibility test coverage — Existing UI tests auditing for accessibility?
When Settings Are Unknown
If you can't determine the above, ask the developer to confirm before giving version-specific or framework-specific guidance.
Quick Decision Tree
When a developer needs accessibility guidance, follow this decision tree:
1. VoiceOver issues?
- Core concepts: Read
references/voiceover.md - UIKit implementation: Read
references/voiceover-uikit.md - SwiftUI implementation: Read
references/voiceover-swiftui.md
2. Dynamic Type, text scaling, or adaptive layout?
- Core concepts: Read
references/dynamic-type.md - UIKit implementation: Read
references/dynamic-type-uikit.md - SwiftUI implementation: Read
references/dynamic-type-swiftui.md
3. Other assistive technologies?
- Voice Control: Read
references/voice-control.md - Switch Control: Read
references/switch-control.md - Full Keyboard Access: Read
references/full-keyboard-access.md
4. Testing accessibility?
- Manual testing: Read
references/testing-manual.md - Automated testing: Read
references/testing-automated.md
5. Cross-cutting concerns?
- Contrast, targets, motion, haptics: Read
references/good-practices.md - Culture and mindset: Read
references/concepts-and-culture.md
6. Quick reference needed?
- Common mistakes, patterns, checklists: Read
references/playbook.md
7. Need definitions or sources?
- Glossary: Read
references/glossary.md - Sources and further reading: Read
references/resources.md
Quick Playbook (Start Here)
1. Confirm framework (UIKit vs SwiftUI) and iOS target. 2. Identify assistive technology and user-experience issue. 3. Use the Decision Tree and jump into the relevant reference file. 4. Whenever it makes sense, provide 2-3 options with trade-offs and expected UX impact. 5. Always include testing guidance alongside any code changes.
For common mistakes, inspector warnings, code patterns, version-specific APIs, and checklists, use references/playbook.md.
Example Prompts and Expected Shape
Example prompt: “VoiceOver reads ‘button’ for my close button.” Expected response:
- Confirm framework and iOS target if unknown.
- Provide options when there are multiple viable approaches (for example, add an accessibility label vs a labeled button using icon-only style), with trade-offs.
- Include a framework-appropriate snippet.
- Add testing steps (VoiceOver, Voice Control...).
Example prompt: “Dynamic Type breaks my header layout in UIKit.” Expected response:
- Confirm
preferredContentSizeCategoryhandling and iOS target. - Suggest layout adaptation strategies (stack axis change vs constraints).
- Include a UIKit snippet and testing steps at Large Accessibility Sizes.
Edge Cases and Gotchas
- Mixed UIKit/SwiftUI screens: use correct API set per view layer.
- Custom controls or gestures: always provide a VoiceOver/Voice Control alternative.
- Unknown iOS target: ask before suggesting version-specific APIs.
- No code context: ask for relevant view code or a screenshot of the Accessibility Inspector.
- Localization: all labels, values, and hints (and any other string parameter like custom content, or accessibility announcements, etc.) must be localized.
interface:
display_name: "iOS Accessibility"
short_description: "iOS accessibility guidance for UIKit and SwiftUI apps"
default_prompt: "Use $ios-accessibility to review this iOS code for VoiceOver, Dynamic Type, and other accessibility issues, then propose fixes, and explain testing steps."
MIT License
Copyright (c) 2025 Daniel Devesa Derksen-Staats
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
Concepts and Culture
Accessibility mindset, inclusive design principles, and organizational practices.
Contents
- Accessibility Is Non-Deterministic
- Inclusive Design
- Models of Disability
- Accessibility Is a Practice
- Culture and Allies
- Common Misconceptions
- Shift Left
- Cross-Assistive Technology Benefits
- Quotes to Remember
- Resources for Continued Learning
- Organizational Practices
- Checklist for Culture
- Sources
Accessibility Is Non-Deterministic
There is no universal "correct" accessible experience. Accessibility is about users and their contexts:
- Different disabilities require different adaptations
- The same user may have different needs in different situations
- Different approaches may have different trade-offs for different assistive technologies
Good practices are defaults, not absolutes. Validate with real users and assistive technologies, then refine based on feedback.
Start with empathy
If a flow is hard with VoiceOver or Switch Control, it’s often hard visually too. Accessibility testing can surface general UX issues earlier.
Four accessibility areas
Apple divides its accessibility features in four categories:
- Vision (blindness, low vision, color blindness)
- Hearing (deafness, hard of hearing)
- Motor (limited dexterity, tremors)
- Cognitive (learning, attention, memory)
Inclusive Design
Inclusive design considers the full range of human diversity from the start — not as an afterthought.
Principles
1. Recognize exclusion: Identify who is being left out and why 2. Solve for one, extend to many: Solutions for specific disabilities often benefit everyone 3. Learn from diversity: Get people with disabilities involved as much as possible
Language and naming
Names and labels shape how inclusive a product feels.
- Prefer neutral, descriptive names over ability-loaded labels
- Avoid framing users as "advanced" vs "limited" based on access needs
- Keep option names specific to behavior (for example, "Guided mode", "Relaxed timing", "High contrast cues")
Example
Curb cuts were designed for wheelchair users but benefit parents with strollers, travelers with luggage, and delivery workers with carts.
Models of Disability
Medical model
Views disability as a defect in the individual that needs fixing.
Social model
Views disability as a mismatch between the person and their environment. The barrier is external, not internal.
Accessibility work often aligns with the social model: remove barriers in products rather than "fix" users.
Accessibility Is a Practice
"Accessibility is both a state and a practice." - Sommer Panage
Treating accessibility as a one-time audit leads to regression. Embedding accessibility into design and engineering systems creates lasting improvement.
Outcomes over one-off outputs
An accessibility backlog or an audit report is useful, but not the goal. The goal is users completing real tasks successfully with assistive technologies.
Use audits as inputs for prioritization and planning, not as an endpoint.
Habits that stick
- Consider accessibility during design, not just at the end (shift left)
- Improve shared components to scale impact
- Automate for guardrails, not full coverage
- Test with assistive technologies regularly
Prioritize by impact
When time is limited:
- Start with top user journeys (onboarding, auth, checkout, media playback, settings)
- Fix blockers before polish
- Prioritize shared components and design tokens for broad impact
- Ship in iterations, then re-test and refine
Culture and Allies
Accessibility specialists can't do it alone. Building a culture means:
- Training designers and engineers on fundamentals
- Creating safe spaces (Slack channels, office hours) for questions
- Celebrating wins and recognizing contributions
- Embedding accessibility into onboarding
Leadership and buy-in
Culture change works best with both bottom-up and top-down support:
- Bottom-up: champions demonstrate practical fixes and mentor others
- Top-down: leadership protects time and includes accessibility in quality expectations
- Communication: explain impact in user outcomes and product risk, not only compliance language
Common Misconceptions
| Misconception | Reality |
|---|---|
| "Accessibility is about improving the experience for VoiceOver users" | There are many assistive technologies, accessibility features, and good practices to take into account |
| "Accessibility is expensive" | Retrofitting is expensive; building accessible from the start is not |
| "Automation covers it" | not all issues are detectable; accessibility often requires judgment |
| "Compliance equals usable" | Passing audits doesn't guarantee a good experience |
Shift Left
Address accessibility earlier in the process:
| Stage | Action |
|---|---|
| Design | Annotate mockups with labels, traits... consider color contrast, how does the interface work for accessibility text sizes |
| Development | Implement accessibility as you build |
| Code review | Check accessibility properties |
| Testing | Manual testing with assistive tech |
| Audit | Validation, not just discovery |
The later issues are found, the more expensive they are to fix.
Cross-Assistive Technology Benefits
APIs like accessibilityLabel, accessibilityTraits, and custom actions benefit multiple technologies:
- VoiceOver
- Voice Control
- Switch Control
- Full Keyboard Access
- Braille displays
Implement once, benefit broadly. Start with VoiceOver — it covers most fundamentals.
Quotes to Remember
"We have one job, and that's to make our apps work. And if you are not implementing accessibility features, you are forgetting about making it work for a lot of people."
— @NovallSwift
"Awareness is the biggest problem here."
— Marco Arment (on accessibility in the Apple ecosystem)
Resources for Continued Learning
- Accessibility Up To 11 — Resources
- Accessibility Up To 11 — #365DaysIOSAccessibility archive
- Fostering An Accessibility Culture (Smashing Magazine)
- WWDC Accessibility videos (Apple)
- Mobile A11y
- Global Accessibility Awareness Day events
Organizational Practices
Champions network
Train advocates across teams who can answer basic questions and escalate complex issues. For larger organizations, a lightweight accessibility guild can coordinate work across teams.
Accessible content
Accessibility isn't just code. Documentation, marketing, and support content need attention too.
Release notes
Call out accessibility improvements in release notes. It signals commitment and invites feedback from users who rely on these features.
Onboarding and training
Include accessibility in onboarding materials so new team members learn it’s part of the definition of quality.
Definition of done
Add “Accessible” to your checklist for shipping a feature. It can be as simple as: “Tested with VoiceOver and Dynamic Type.”
Workshops and lunch‑and‑learns
Share improvements and lessons learned across the team. Short demos of real fixes are often the most effective. Watch "Convenience for You is Independence for Me" (WWDC 2017) as a team — Todd Stabelfeldt's story of living with quadriplegia and how apps changed his independence is consistently impactful.
Code review and QA
Encourage reviewers to flag accessibility issues early. Check out branches and run the app — catching a bug at review time is much faster than after release. Add quick manual tests (VoiceOver, Dynamic Type) to QA smoke checks.
In product discussions and design reviews, keep asking: "What does this look like at the largest accessibility font size? Are any key actions hidden behind gestures? Do we have copy for all interactive elements?" A few repetitions and the team will start asking these themselves.
Hiring and workplace
People with disabilities face barriers in hiring processes and workplace tools. Improving these creates a more inclusive team that builds more inclusive products. Consider adding accessibility knowledge as a requirement or a positive differentiator in iOS developer job descriptions — it raises awareness in the community at scale.
Audit document
A practical way to get started is to put your headphones on, turn VoiceOver on, and navigate through the most important flows in your app. Document what you find in a shared document — organized by screen or feature. Keep completed fixes rather than deleting them (cross them out or add a ✅). This serves two purposes: it communicates the current state visually (a long list is a clear signal), and it gives teammates a direct starting point when they ask "how can I help?"
Once you have the list, share it in a sprint demo, all-hands, or internal meetup. Show a particularly bad experience, then show it fixed. Most fixes take just a few lines of code. Seeing that directly makes it difficult for anyone to argue it's too complex or expensive to do.
Demo with assistive technologies to raise awareness with the rest of the team on the diversity of ways users interact with products.
Sustainability and burnout prevention
Accessibility work is long-term. Avoid hero culture and distribute ownership:
- Define realistic iteration goals instead of "fix everything now"
- Celebrate incremental wins
- Keep decision logs so knowledge persists when people move teams
Checklist for Culture
- [ ] Accessibility considered in design phase
- [ ] Engineers trained on fundamentals
- [ ] Safe space for questions (Slack, office hours)
- [ ] Wins celebrated and contributions recognized
- [ ] Regular manual testing with assistive tech
- [ ] Shared components improved for accessibility
- [ ] Accessibility included in definition of done
- [ ] Accessibility improvements mentioned in release notes
Sources
Dynamic Type — SwiftUI
SwiftUI implementation for Dynamic Type and scalable layouts.
For core concepts, see dynamic-type.md.
Contents
- Text Styles
- Layout Adaptation
- Scale Non-Text Elements
- Large Content Viewer
- Constrained Dynamic Type
- Testing
- Examples
Text Styles
SwiftUI automatically scales text with text styles:
Text("Hello")
.font(.body)No additional configuration needed — text scales automatically. For exact point sizes by text style and content size category, see Apple HIG: iOS/iPadOS Dynamic Type sizes.
All Text Styles
Text("Large Title").font(.largeTitle)
Text("Title").font(.title)
Text("Title 2").font(.title2)
Text("Title 3").font(.title3)
Text("Headline").font(.headline)
Text("Subheadline").font(.subheadline)
Text("Body").font(.body)
Text("Callout").font(.callout)
Text("Footnote").font(.footnote)
Text("Caption").font(.caption)
Text("Caption 2").font(.caption2)Custom Fonts
Scale custom fonts relative to a text style:
Text("Custom")
.font(.custom("PressStart2P-Regular", size: 17, relativeTo: .body))Detect Accessibility Sizes
For the larger accessibility categories and their reference sizes, see Apple HIG: iOS/iPadOS larger accessibility type sizes.
@Environment(\.dynamicTypeSize) var dynamicTypeSize
var body: some View {
if dynamicTypeSize.isAccessibilitySize {
// Accessibility size (one of the 5 largest)
}
}Compare sizes
if dynamicTypeSize >= .accessibility1 {
// First accessibility size or larger
}Layout Adaptation
At larger text sizes, switch from horizontal to vertical layouts so text can flow across the full screen width.
Flip stack axis
@Environment(\.dynamicTypeSize) var dynamicTypeSize
var body: some View {
let layout = dynamicTypeSize.isAccessibilitySize
? AnyLayout(VStackLayout())
: AnyLayout(HStackLayout())
layout {
Image(systemName: "star")
Text("Favorite")
}
}Approach with Group
@Environment(\.dynamicTypeSize) var dynamicTypeSize
var body: some View {
Group {
if dynamicTypeSize.isAccessibilitySize {
VStack { content }
} else {
HStack { content }
}
}
}
@ViewBuilder
var content: some View {
Image(systemName: "star")
Text("Favorite")
}ViewThatFits
Let SwiftUI choose the best layout automatically:
ViewThatFits {
HStack { content } // Try horizontal first
VStack { content } // Fall back to vertical
}Use this when layout should adapt to available space and actual content fit, not only to a specific dynamicTypeSize or size class threshold. It is also a strong option when the fallback is more complex than simply switching HStack to VStack (for example, re-grouping content, changing hierarchy, or dropping non-essential decorative elements).
Great for local UI blocks that appear once (or only a few times) on screen. For repeated rows in lists/grids, prefer a deterministic rule (for example, dynamic type threshold) so items do not switch layout inconsistently from one row to another.
ScrollView for oversized content
Regardless of the complexity of the screen, consider wrapping the screen in a scroll view so there is always room for the content, even for accessibility sizes:
@Environment(\.dynamicTypeSize) var dynamicTypeSize
var body: some View {
Group {
if dynamicTypeSize.isAccessibilitySize {
ScrollView { content }
} else {
content
}
}
}Reusable AdaptiveStack component
Create a reusable component that considers both size class and Dynamic Type:
public struct AdaptiveStack<Content: View>: View {
@Environment(\.horizontalSizeClass) var horizontalSizeClass
@Environment(\.dynamicTypeSize) var dynamicTypeSize
private let horizontalAlignment: HorizontalAlignment
private let verticalAlignment: VerticalAlignment
private let spacing: CGFloat?
private let content: Content
public init(
horizontalAlignment: HorizontalAlignment = .center,
verticalAlignment: VerticalAlignment = .center,
spacing: CGFloat? = nil,
@ViewBuilder content: () -> Content
) {
self.horizontalAlignment = horizontalAlignment
self.verticalAlignment = verticalAlignment
self.spacing = spacing
self.content = content()
}
public var body: some View {
if dynamicTypeSize.isAccessibilitySize {
VStack(alignment: horizontalAlignment, spacing: spacing) { content }
} else {
HStack(alignment: verticalAlignment, spacing: spacing) { content }
}
}
}Usage:
AdaptiveStack(horizontalAlignment: .leading, spacing: 12) {
Image(systemName: "info.circle")
.font(.title2)
VStack(alignment: .leading) {
Text("About")
Text("App information")
.font(.caption)
}
}Extend with additional conditions like compact size class:
public enum AdaptiveCondition {
case accessible // Accessibility text sizes
case compact // Compact width
case compactAccessible // Both
}Source: SwiftUI Adaptive Stack Views - Use Your Loaf
Example: List row
struct DrinkTableRow: View {
let drink: Drink
@Environment(\.dynamicTypeSize.isAccessibilitySize) var accessibilitySize
var body: some View {
NavigationLink {
DrinkDetail(drink: drink)
} label: {
// Adapt layout for large text
if accessibilitySize {
VStack(alignment: .leading) {
DrinkTableRowContent(drink: drink)
}
} else {
HStack {
DrinkTableRowContent(drink: drink)
}
}
}
}
}Example: Stepper control
struct ExtraShotsView: View {
@State private var shots = 0
var body: some View {
ViewThatFits {
HStack {
Image(systemName: "minus.circle")
Text("\(shots) shots")
Image(systemName: "plus.circle")
Text("+ £\(shots * 0.50, format: .currency(code: "GBP"))")
}
VStack {
HStack {
Image(systemName: "minus.circle")
Text("\(shots) shots")
Image(systemName: "plus.circle")
}
Text("+ £\(shots * 0.50, format: .currency(code: "GBP"))")
}
}
}
}Multiline Text
SwiftUI Text wraps by default. For TextField:
TextField("Notes", text: $notes, axis: .vertical)
.lineLimit(3...10)If you must cap lines for product reasons, relax the cap at larger sizes (for example, double or triple it for accessibility sizes):
@Environment(\.dynamicTypeSize) private var dynamicTypeSize
private var titleLineLimit: Int {
if dynamicTypeSize >= .accessibility3 { return 6 } // triple from 2
if dynamicTypeSize.isAccessibilitySize { return 4 } // double from 2
return 2
}
Text(title)
.lineLimit(titleLineLimit)Scale Non-Text Elements
Use ScaledMetric for icons, spacing, and borders:
@ScaledMetric(relativeTo: .body) var iconSize: CGFloat = 24
Image(systemName: "star")
.frame(width: iconSize, height: iconSize)Match scaling to text style
Use relativeTo: to tie scaling to specific text styles:
@ScaledMetric(relativeTo: .title3) private var borderWidth: CGFloat = 3.0
@ScaledMetric(relativeTo: .body) private var iconSize: CGFloat = 24
@ScaledMetric(relativeTo: .largeTitle) private var headerSpacing: CGFloat = 16This ensures visual elements scale proportionally with their associated text.
Example: Scaled border
struct TranscriptLineView: View {
@ScaledMetric(relativeTo: .title3) private var baseBorderWidth: CGFloat = 3.0
@Environment(\.legibilityWeight) private var legibilityWeight
private var borderWidth: CGFloat {
// Double border width when Bold Text is enabled
legibilityWeight == .bold ? baseBorderWidth * 2 : baseBorderWidth
}
var body: some View {
content
.overlay(
RoundedRectangle(cornerRadius: 8)
.strokeBorder(.tint, lineWidth: borderWidth)
)
}
}Relative Frame
Use container-relative sizing:
Text("Content")
.containerRelativeFrame(.horizontal) { length, _ in
length * 0.8
}Example: Adaptive Card
struct CardView: View {
let title: String
let subtitle: String
@Environment(\.dynamicTypeSize) var dynamicTypeSize
@ScaledMetric(relativeTo: .body) var imageSize: CGFloat = 60
var body: some View {
Group {
if dynamicTypeSize.isAccessibilitySize {
VStack(alignment: .leading) { content }
} else {
HStack { content }
}
}
.padding()
}
@ViewBuilder
var content: some View {
Image(systemName: "photo")
.frame(width: imageSize, height: imageSize)
VStack(alignment: .leading) {
Text(title)
.font(.headline)
Text(subtitle)
.font(.subheadline)
.foregroundStyle(.secondary)
}
}
}Example: Adaptive Grid
struct AdaptiveGridView: View {
let items: [Item]
@Environment(\.dynamicTypeSize) var dynamicTypeSize
var columns: [GridItem] {
let count = dynamicTypeSize.isAccessibilitySize ? 1 : 2
return Array(repeating: GridItem(.flexible()), count: count)
}
var body: some View {
LazyVGrid(columns: columns) {
ForEach(items) { item in
ItemView(item: item)
}
}
}
}Preview with Different Sizes
Preview all Dynamic Type sizes:
#Preview {
ContentView()
}Use the Xcode preview toolbar, go to "Variants" and "Dynamic Type Variaants" to preview the layout for all sizes.
Explicit size in preview
#Preview {
ContentView()
.dynamicTypeSize(.accessibility3)
}Large Content Viewer
For bar items and other elements that don't scale, provide a Large Content Viewer:
Button { showBasket.toggle() } label: {
ZStack(alignment: .topTrailing) {
Image(systemName: "cart.fill")
if basket.orderCount > 0 {
Text("\(basket.orderCount)")
.padding(5)
.background(.red)
.clipShape(Capsule())
}
}
}
.accessibilityShowsLargeContentViewer {
Image(systemName: "cart.fill")
Text("Cart, \(basket.orderCount) items")
}Users with Larger Accessibility Sizes can tap-and-hold to see the enlarged content in the center of the screen. Use high-quality vector assets (for example SF Symbols or vector PDFs) so enlarged previews stay crisp.
Conditional Modifier Pattern
A reusable pattern for conditionally applying modifiers based on accessibility settings:
extension View {
@ViewBuilder
func `if`<Content: View>(_ condition: Bool, transform: (Self) -> Content) -> some View {
if condition {
transform(self)
} else {
self
}
}
}Usage for accessibility modifiers:
@Environment(\.verticalSizeClass) private var verticalSizeClass
private var isLandscape: Bool {
verticalSizeClass == .compact
}
var body: some View {
Slider(value: $progress)
.if(!isLandscape) { view in
view.accessibilityShowsLargeContentViewer()
}
}This avoids duplicating views and keeps conditional logic readable.
Constrained Dynamic Type
For elements that shouldn't scale beyond a certain size (this should be avoided, and you should have a very good reason or alternative to do it), constrain the Dynamic Type size:
Slider(value: $progress)
.dynamicTypeSize(.large) // Cap at Large size
.accessibilityShowsLargeContentViewer() // Provide alternative for larger sizesExample: Progress slider
Slider(value: $sliderValue, in: 0...duration)
.accessibilityLabel("Progress")
.accessibilityValue(currentLineText)
.if(!isLandscape) { view in
view.dynamicTypeSize(.large)
.accessibilityShowsLargeContentViewer()
}Always pair constrained elements with Large Content Viewer so users with accessibility sizes can still access the information.
Minimum Scale Factor
Allow text to shrink slightly before wrapping (use sparingly):
Text("Long title that might not fit")
.minimumScaleFactor(0.8)Testing
Environment Overrides
In Xcode's Debug Area toolbar, click Environment Overrides to change Dynamic Type size.
Simulator shortcut
Option + Command + +/- to increase/decrease text size.
Sources
- https://accessibilityupto11.com/365-days-ios-accessibility/
- https://accessibilityupto11.com/blog/
- https://github.com/dadederk/fromZeroToAccessible (Daniel Devesa Derksen-Staats and Rob Whitaker)
Dynamic Type — UIKit
UIKit implementation for Dynamic Type and scalable layouts.
For core concepts, see dynamic-type.md.
Contents
- Text Styles
- Custom Fonts
- Layout Adaptation
- Scale Non-Text Elements
- Large Content Viewer
- Web Content
- Examples
Text Styles
label.font = UIFont.preferredFont(forTextStyle: .body)
label.adjustsFontForContentSizeCategory = trueImportant: Set adjustsFontForContentSizeCategory = true for automatic updates when the user changes text size. For exact point sizes by text style and content size category, see Apple HIG: iOS/iPadOS Dynamic Type sizes.
Custom Fonts
Use UIFontMetrics to scale custom fonts:
let customFont = UIFont(name: "Avenir-Medium", size: 17)!
let fontMetrics = UIFontMetrics(forTextStyle: .body)
label.font = fontMetrics.scaledFont(for: customFont)
label.adjustsFontForContentSizeCategory = trueMultiline Labels
Allow text to wrap:
label.numberOfLines = 0Avoid fixed height constraints.
If you must cap lines for product reasons, relax the cap at larger sizes (for example, double or triple it for accessibility categories):
func updateLineLimit(for category: UIContentSizeCategory) {
switch category {
case .accessibilityExtraExtraExtraLarge:
titleLabel.numberOfLines = 6 // triple from 2
case .accessibilityMedium, .accessibilityLarge, .accessibilityExtraLarge, .accessibilityExtraExtraLarge:
titleLabel.numberOfLines = 4 // double from 2
default:
titleLabel.numberOfLines = 2
}
}Detect Accessibility Sizes
For the larger accessibility categories and their reference sizes, see Apple HIG: iOS/iPadOS larger accessibility type sizes.
if traitCollection.preferredContentSizeCategory.isAccessibilityCategory {
// Accessibility size (one of the 5 largest)
}Compare size categories
if traitCollection.preferredContentSizeCategory >= .accessibilityLarge {
// Large or larger
}Respond to Size Changes
traitCollectionDidChange
override func traitCollectionDidChange(_ previous: UITraitCollection?) {
super.traitCollectionDidChange(previous)
if traitCollection.preferredContentSizeCategory != previous?.preferredContentSizeCategory {
updateLayout()
}
}Notification
NotificationCenter.default.addObserver(
self,
selector: #selector(handleSizeChange),
name: UIContentSizeCategory.didChangeNotification,
object: nil
)Layout Adaptation
At larger text sizes, switch from horizontal to vertical layouts so text can flow across the full screen width.
Flip stack axis
func updateLayout() {
stackView.axis = traitCollection.preferredContentSizeCategory.isAccessibilityCategory
? .vertical
: .horizontal
}Listen for changes
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
if previousTraitCollection?.preferredContentSizeCategory != traitCollection.preferredContentSizeCategory {
updateLayout()
}
}Example: Table cell
final class DrinkTableViewCell: UITableViewCell {
@IBOutlet private weak var outerStackView: UIStackView!
@IBOutlet private weak var drinkNameLabel: UILabel!
override func awakeFromNib() {
super.awakeFromNib()
// Dynamic Type fonts
drinkNameLabel.font = .preferredFont(forTextStyle: .body)
drinkNameLabel.adjustsFontForContentSizeCategory = true
updateLayout()
}
override func traitCollectionDidChange(_ previous: UITraitCollection?) {
if previous?.preferredContentSizeCategory != traitCollection.preferredContentSizeCategory {
updateLayout()
}
}
private func updateLayout() {
if traitCollection.preferredContentSizeCategory.isAccessibilityCategory {
outerStackView.axis = .vertical
outerStackView.alignment = .leading
drinkNameLabel.numberOfLines = 0 // Unlimited lines
} else {
outerStackView.axis = .horizontal
outerStackView.alignment = .center
drinkNameLabel.numberOfLines = 1
}
}
}Example: Stepper control
class ExtraShotsView: UIView {
@IBOutlet private weak var mainStackView: UIStackView!
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
if traitCollection.preferredContentSizeCategory.isAccessibilityCategory {
mainStackView.axis = .vertical
} else {
mainStackView.axis = .horizontal
}
}
}Fallback: Scroll view for oversized content
If large text still doesn’t fit, embed the screen in a scroll view for accessibility sizes:
func updateLayout() {
if traitCollection.preferredContentSizeCategory.isAccessibilityCategory {
contentScrollView.isScrollEnabled = true
} else {
contentScrollView.isScrollEnabled = false
}
}Switch to single column
let columns = traitCollection.preferredContentSizeCategory.isAccessibilityCategory ? 1 : 2Constraint sets
Create separate constraint sets and activate based on size:
var defaultConstraints: [NSLayoutConstraint] = []
var accessibilityConstraints: [NSLayoutConstraint] = []
func updateConstraints() {
if traitCollection.preferredContentSizeCategory.isAccessibilityCategory {
NSLayoutConstraint.deactivate(defaultConstraints)
NSLayoutConstraint.activate(accessibilityConstraints)
} else {
NSLayoutConstraint.deactivate(accessibilityConstraints)
NSLayoutConstraint.activate(defaultConstraints)
}
}Readable Content Guide
For long-form text, constrain to readableContentGuide for comfortable line length:
textView.leadingAnchor.constraint(equalTo: view.readableContentGuide.leadingAnchor).isActive = true
textView.trailingAnchor.constraint(equalTo: view.readableContentGuide.trailingAnchor).isActive = trueBaseline Spacing
Use system baseline spacing instead of fixed constants:
subtitleLabel.firstBaselineAnchor.constraint(
equalToSystemSpacingBelow: titleLabel.lastBaselineAnchor,
multiplier: 1.0
).isActive = trueScale Non-Text Elements
Use UIFontMetrics.scaledValue(for:) for icons and other UI:
let baseHeight: CGFloat = 20
let scaledHeight = UIFontMetrics.default.scaledValue(for: baseHeight)
progressView.heightAnchor.constraint(equalToConstant: scaledHeight).isActive = trueScale Images
imageView.adjustsImageSizeForAccessibilityContentSizeCategory = trueUse PDF/vector assets with Preserve Vector Data enabled.
Prefer SF Symbols with text styles
SF Symbols scale like fonts and can be tied to a text style:
iconImageView.image = UIImage(systemName: "xmark.octagon")
iconImageView.preferredSymbolConfiguration = UIImage.SymbolConfiguration(textStyle: .body)This keeps icon size in sync with the adjacent text.
Large Content Viewer
For elements that don't scale (bars, compact controls), let users tap-and-hold to see enlarged content.
Use this when the UI cannot scale with Dynamic Type (navigation bars, tab bars, toolbars). If content can scale, prefer Dynamic Type.
Tab bar assets for large preview
If you can’t provide vector PDFs, use a larger raster image for the preview:
// Provide a higher-resolution image for the large preview
tabBarItem.largeContentSizeImage = UIImage(named: "tab-large")Custom bar elements
Custom views need explicit titles/images:
customBarView.addInteraction(UILargeContentViewerInteraction())
customBarButton.showsLargeContentViewer = true
customBarTabView.showsLargeContentViewer = true
customBarTabView.largeContentTitle = "Videos"
customBarTabView.largeContentImage = UIImage(named: "play")Protocol implementation
class CustomTabItem: UIView, UILargeContentViewerItem {
var showsLargeContentViewer: Bool { true }
var largeContentTitle: String? { "Home" }
var largeContentImage: UIImage? { UIImage(systemName: "house") }
}
// Add interaction to the container
tabBar.addInteraction(UILargeContentViewerInteraction())Example: Cart button with badge
class OrderButtonView: UIView {
@IBOutlet private weak var orderButton: UIButton!
private var numberOfItems: UInt = 0 {
didSet {
// Update Large Content Viewer with current count
orderButton.largeContentTitle = "Cart, \(numberOfItems) items"
}
}
func enableLargeContentViewer() {
orderButton.showsLargeContentViewer = true
orderButton.addInteraction(UILargeContentViewerInteraction())
}
}Users with Larger Accessibility Sizes can tap-and-hold to see the button's content displayed larger in the center of the screen.
Web Content
In WKWebView, use Apple system fonts in CSS — they respect Dynamic Type automatically on Apple devices. Always include fallback fonts for cross-platform HTML:
body {
font: -apple-system-body;
}
h1 {
font: -apple-system-headline;
color: darkblue;
}
.footnote {
font: -apple-system-footnote;
color: gray;
}The web content won't resize automatically when the user changes their text size preference. Listen for the UIContentSizeCategory.didChangeNotification and reload the page:
class WebViewController: UIViewController {
@IBOutlet weak var webView: WKWebView!
override func viewDidLoad() {
super.viewDidLoad()
loadContent()
NotificationCenter.default.addObserver(
self,
selector: #selector(contentSizeCategoryDidChange),
name: UIContentSizeCategory.didChangeNotification,
object: nil
)
}
private func loadContent() {
guard let baseURL = Bundle.main.resourceURL else { return }
let fileURL = baseURL.appendingPathComponent("content.html")
webView.loadFileURL(fileURL, allowingReadAccessTo: fileURL)
}
@objc private func contentSizeCategoryDidChange() {
webView.reload()
}
}A full list of Apple CSS font names is documented at webkit.org/blog/3709/using-the-system-font-in-web-content/.
Interface Builder
Configure Dynamic Type in Interface Builder: 1. Select the label 2. In Attributes Inspector, choose a text style for Font 3. Check "Automatically Adjusts Font"
Example: Adaptive Card
class CardView: UIView {
let stackView = UIStackView()
let imageView = UIImageView()
let titleLabel = UILabel()
let subtitleLabel = UILabel()
override init(frame: CGRect) {
super.init(frame: frame)
titleLabel.font = UIFont.preferredFont(forTextStyle: .headline)
titleLabel.adjustsFontForContentSizeCategory = true
titleLabel.numberOfLines = 0
subtitleLabel.font = UIFont.preferredFont(forTextStyle: .subheadline)
subtitleLabel.adjustsFontForContentSizeCategory = true
subtitleLabel.numberOfLines = 0
stackView.axis = .horizontal
stackView.spacing = 12
stackView.addArrangedSubview(imageView)
stackView.addArrangedSubview(titleLabel)
stackView.addArrangedSubview(subtitleLabel)
addSubview(stackView)
updateLayout()
}
override func traitCollectionDidChange(_ previous: UITraitCollection?) {
super.traitCollectionDidChange(previous)
updateLayout()
}
func updateLayout() {
stackView.axis = traitCollection.preferredContentSizeCategory.isAccessibilityCategory
? .vertical
: .horizontal
}
}Sources
- https://accessibilityupto11.com/365-days-ios-accessibility/
- https://github.com/Apress/developing-accessible-iOS-apps
- https://github.com/dadederk/fromZeroToAccessible (Daniel Devesa Derksen-Staats and Rob Whitaker)
Dynamic Type
Core concepts for scalable text and adaptive layouts on iOS.
What Is Dynamic Type
Dynamic Type lets users choose their preferred text size. iOS scales text automatically when you use text styles.
Text Styles
iOS provides semantic text styles that scale together:
| Style | Typical Use |
|---|---|
.largeTitle | Screen titles |
.title, .title2, .title3 | Section headings |
.headline | Emphasized body text |
.subheadline | Secondary labels |
.body | Main content |
.callout | Supplementary descriptions |
.footnote | Tertiary info |
.caption, .caption2 | Metadata, timestamps |
Use text styles instead of fixed font sizes. For exact point sizes by text style and content size category, see Apple HIG: iOS/iPadOS Dynamic Type sizes.
Text styles do not scale linearly with a fixed ratio across categories. Avoid assumptions like "title is always X times body" at larger sizes.
Larger Accessibility Sizes
Beyond the 7 standard sizes, iOS offers 5 additional Accessibility Sizes. Users enable them in Settings > Accessibility > Display & Text Size > Larger Text. Reference sizes are documented in Apple HIG: iOS/iPadOS larger accessibility type sizes.
Always test with these larger sizes — they reveal layout issues that standard sizes don't.
Test the worst case
Dynamic Type issues often show up in edge screens like empty states, error views, and popovers.
- Test at Accessibility 5 (largest size)
- Test with longer localized strings (e.g., German, Spanish)
- Check loading/error screens, not just primary flows
Layout Adaptation
At larger sizes, layouts often need to change:
- Horizontal stacks become vertical
- Multi-column grids become single column
- More lines of text are allowed
- Elements may need to wrap
Large Content Viewer
Navigation bars, tab bars, and toolbars don't scale with Dynamic Type. Users can long-press to see a magnified label via Large Content Viewer.
Testing
1. Use Control Center's Text Size control
- Test app-specific overrides as well (Text Size can be adjusted per app from Control Center)
2. Test with Larger Accessibility Sizes enabled 3. Use Xcode's Environment Overrides 4. Use Accessibility Inspector's Settings tab 5. Try the Double-Length Pseudolanguage for stress testing
Implementation
For UIKit implementation, see dynamic-type-uikit.md.
For SwiftUI implementation, see dynamic-type-swiftui.md.
Sources
- https://accessibilityupto11.com/365-days-ios-accessibility/
- https://github.com/Apress/developing-accessible-iOS-apps
Full Keyboard Access
Full Keyboard Access enables users to navigate and interact with iOS using only a hardware keyboard.
How It Works
Users navigate through focusable elements with Tab and activate with Space. A visible focus indicator shows the current element.
Full Keyboard Access is especially important on iPadOS with hardware keyboards.
Enable Full Keyboard Access
Settings > Accessibility > Keyboards > Full Keyboard Access
Key Commands
| Key | Action |
|---|---|
| Tab | Move to next element |
| Shift + Tab | Move to previous element |
| Space | Activate focused element |
| Escape | Dismiss or go back |
| Tab + Z | Show custom actions |
| Arrow keys | Navigate within controls |
Development Impact
Elements that work with VoiceOver typically work with Full Keyboard Access. Focus on:
- Logical focus order
- Visible focus indicator
- Keyboard shortcuts for key actions
Focus Order
Focus follows the accessibility order. Use the same techniques as VoiceOver:
UIKit:
view.accessibilityElements = [headerLabel, searchField, listView]SwiftUI:
VStack {
Text("First").accessibilitySortPriority(2)
Text("Second").accessibilitySortPriority(1)
}Keyboard Shortcuts
Provide shortcuts for frequently used actions.
UIKit
override var keyCommands: [UIKeyCommand]? {
[
UIKeyCommand(
title: "Refresh",
action: #selector(refresh),
input: "r",
modifierFlags: .command
),
UIKeyCommand(
title: "Search",
action: #selector(search),
input: "f",
modifierFlags: .command
)
]
}SwiftUI
Button("Refresh", action: refresh)
.keyboardShortcut("r", modifiers: .command)Shortcuts appear when the user holds the Command key.
Input Labels
Help users find elements by alternate names:
UIKit:
button.accessibilityUserInputLabels = ["Settings", "Preferences", "Config"]SwiftUI:
.accessibilityInputLabels(["Settings", "Preferences", "Config"])This helps keyboard users who search by name.
Custom Actions
Custom actions are accessible via Tab + Z:
cell.accessibilityCustomActions = [
UIAccessibilityCustomAction(name: "Delete", actionHandler: { _ in
self.delete()
return true
})
]Grouping
Group related elements to reduce Tab stops:
UIKit:
containerView.isAccessibilityElement = true
containerView.accessibilityLabel = "\(title), \(subtitle)"SwiftUI:
.accessibilityElement(children: .combine)Testing
Simulator
Enable Full Keyboard Access in the simulator's Settings app. Use your Mac keyboard to navigate.
Device
Connect a hardware keyboard (Bluetooth or Smart Connector).
What to verify
1. All interactive elements are focusable 2. Focus order is logical 3. Focus indicator is visible 4. Space activates the focused element 5. Escape dismisses modals 6. Keyboard shortcuts work
Common Issues
| Problem | Solution |
|---|---|
| Element not focusable | Ensure isAccessibilityElement = true |
| Focus order confusing | Set accessibilityElements order |
| Focus indicator hidden | Avoid clipping or overlays |
| Action requires touch | Add keyboard shortcut or custom action |
Checklist
- [ ] All interactive elements focusable
- [ ] Focus order follows task flow
- [ ] Keyboard shortcuts for main actions
- [ ] Custom actions exposed via Tab + Z
- [ ] Tested in simulator and on device
Sources
- https://accessibilityupto11.com/365-days-ios-accessibility/
- https://accessibilityupto11.com/blog/
Accessibility Glossary
Quick reference for common accessibility terms and concepts.
Assistive Technologies
VoiceOver — Apple's screen reader. Reads screen content aloud and allows navigation via gestures or keyboard. Users with blindness or low vision rely on VoiceOver to use iOS apps.
Voice Control — System-wide voice navigation and dictation. Recognizes accessibility labels and allows full device control without internet.
Switch Control — Scanning-based navigation for external switches. Users cycle through elements and activate with a switch press. Critical for users with severe motor disabilities.
Full Keyboard Access — Navigate and control iOS with an external keyboard. Useful for users who cannot use touch input.
Dynamic Type — System-wide text size control. Users choose from 12 sizes (7 standard + 5 accessibility sizes). Text scales from -3 to +5 relative to default. See Apple HIG for exact sizes: iOS/iPadOS Dynamic Type sizes and iOS/iPadOS larger accessibility type sizes.
Zoom — System-wide screen magnification. Users can zoom in up to 15x. Different from pinch-to-zoom within apps.
Large Content Viewer — Tap-and-hold interface for elements that don't scale. Shows enlarged version in center of screen. iOS 13+ (`UILargeContentViewerInteraction`).
Guided Access — Locks the device to a single app and can restrict touch areas. Useful for education, kiosks, or focused tasks.
Magnifier — Camera-based zoom tool for viewing real-world content. Useful for reading printed text or signs.
Assistive Touch (AssistiveTouch) — On-screen menu for gestures and actions. Reduces need for complex multi-finger gestures.
Accessibility Properties
accessibilityLabel — The name of an element. Answers "What is this?" Examples: "Close", "Play", "Settings". Should be concise and not include the control type.
accessibilityValue — The current state or value. Answers "What's its current state?" Examples: "50 percent", "On", "Line 3 of 10". Updates as state changes.
accessibilityHint — Description of the result of an action. Answers "What happens when I use it?" Examples: "Plays the audio", "Opens settings". Optional; use sparingly for non-obvious actions.
accessibilityTraits — Characteristics that describe the element's role and state. Examples: .button, .header, .selected, .adjustable. Multiple traits can be combined.
accessibilityCustomActions — Secondary actions available through VoiceOver's Actions rotor. Example: Delete, Share, Mark as Read. iOS 8+ (`UIAccessibilityCustomAction`).
accessibilityElements — Ordered array of accessible elements within a container. Used to control navigation order when automatic ordering isn't correct.
Common Traits
.button — Tappable control that performs an action
.header — Section heading (shows in VoiceOver's Headings rotor)
.selected — Currently selected item in a group (picker, tabs, segmented control)
.adjustable — Value can be incremented/decremented (sliders, steppers, pickers)
.link — Opens a URL or navigates
.isModal — Modal dialog that restricts focus
.updatesFrequently — Value changes rapidly (prevents VoiceOver interruptions)
.startsMediaSession — Plays audio/video when activated
.allowsDirectInteraction — Passes touches through (for drawing, piano apps)
Accessibility Features (iOS Settings)
Reduce Motion — Minimizes animations and parallax effects. Users enable to reduce vestibular triggers and motion sickness.
Reduce Transparency — Makes translucent backgrounds opaque. Improves contrast and reduces visual complexity.
Increase Contrast — Increases color contrast throughout the system. Helps users with low vision distinguish elements.
Differentiate Without Color — Adds shapes/icons alongside color to convey meaning. Essential for color-blind users.
Bold Text — Makes system fonts bolder. Requires app restart.
Button Shapes — Adds outlines/underlines to buttons. Helps identify interactive elements.
Smart Invert Colors — Inverts colors except for images and media. Alternative to Dark Mode for high contrast.
Invert Colors — Classic color inversion that affects all content, including images.
Mono Audio — Plays audio channels in mono. Useful for users with hearing loss in one ear.
LED Flash for Alerts — Flashes the camera LED for notifications (helpful for deaf or hard-of-hearing users).
Audio Descriptions — Narration that describes visual content in videos.
Made for iPhone Hearing Aids — Lets users stream audio directly to compatible hearing aids.
Larger Accessibility Sizes — Five text sizes beyond the standard Large (from XXL to Accessibility 5).
Text Styles (Dynamic Type)
Standard Sizes:
- Large Title (34pt default)
- Title (28pt)
- Title 2 (22pt)
- Title 3 (20pt)
- Headline (17pt bold)
- Body (17pt) — base size
- Callout (16pt)
- Subheadline (15pt)
- Footnote (13pt)
- Caption (12pt)
- Caption 2 (11pt)
All scale proportionally with user's text size setting.
Concepts
Accessibility Element — Any UI component that assistive technologies can interact with. By default, standard controls (buttons, labels) are accessibility elements; custom views are not.
Focus — The current element that VoiceOver or Full Keyboard Access is highlighting. Only one element has focus at a time.
Rotor — VoiceOver gesture (two-finger rotation) that reveals a context menu. Common rotors: Headings, Links, Form Controls, Landmarks, Custom Actions.
Semantic Colors — System colors that adapt to Light/Dark mode and Increase Contrast. Examples: .label, .systemBackground, .secondaryLabel.
Hit Area — The tappable region of a control. Should be at least 44×44 points for accessibility.
Isolation Domain — In Swift Concurrency context: main actor vs background actors. For accessibility: ensuring UI updates happen on main thread.
Grouping — Combining multiple elements into one accessibility element. Reduces swipe count and makes navigation easier.
Accessibility Container — A view that contains other accessibility elements and can control their order.
Testing Terms
Accessibility Inspector — Xcode tool for inspecting accessibility properties and running audits. Window > Accessibility Inspector.
Accessibility Identifier — String used to identify elements in UI tests. Not read by VoiceOver; purely for automation.
Environment Overrides — Xcode feature to test different accessibility settings without changing system settings.
Audit — Accessibility Inspector feature that checks for common issues: missing labels, low contrast, small targets.
Caption Panel — VoiceOver feature showing speech output as text. Settings > Accessibility > VoiceOver > Caption Panel.
Screen Curtain — VoiceOver feature that turns off the display while keeping the phone functional. Three-finger triple-tap.
Abbreviations
AT — Assistive Technology
A11y — Numeronym for "accessibility" (a + 11 letters + y)
VO — VoiceOver
DT — Dynamic Type
WCAG — Web Content Accessibility Guidelines (applies to iOS apps too)
ARIA — Accessible Rich Internet Applications (web standard; iOS equivalents exist)
Related Terms
Inclusive Design — Design approach that considers diverse human abilities from the start, not as an afterthought.
Universal Design — Design usable by all people, to the greatest extent possible, without adaptation.
Disability — Mismatch between a person's abilities and their environment. Accessibility removes barriers.
Permanent, Temporary, Situational — Types of disabilities. Example: blind (permanent), eye injury (temporary), bright sunlight (situational).
Social Model of Disability — Framework that views disability as created by barriers in society, not by the person's impairment.
Sources
Good Practices
Cross-cutting accessibility guidance: touch targets, color contrast, motion, transparency, haptics, and multi-modal design.
Contents
- Touch Target Size
- Color Contrast
- Don't Rely on Color Alone
- Avoid Text in Images
- Media Accessibility
- Hearing Accommodations
- Reduce Motion
- Reduce Transparency
- Video Playback Preferences
- Semantic Colors
- Bold Text
- Button Shapes
- Haptic Feedback
- Keyboard Shortcuts
- Multi-Modal Information
- Multiple Input Paths
- Orientation Support
- Avoid Ephemeral Feedback
- Alt Text for Shared Images
- Smart Invert
- Invert Colors (Classic)
- Toggles and Switches
- Watchables and Wearables
- Checklist
Touch Target Size
Apple recommends a minimum tappable area of 44×44 points.
Also try to keep targets at least 32 points apart to reduce accidental taps, especially for users with tremors or low vision.
If you can’t increase spacing, increase the hit area using insets or contentShape.
Common violations
- Navigation bar buttons
- Custom toolbar icons
- Dismiss/close buttons
- Inline text links
Fix small targets
Expand the hit area without changing appearance:
UIKit:
button.contentEdgeInsets = UIEdgeInsets(top: 12, left: 12, bottom: 12, right: 12)Or override point(inside:with:):
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
bounds.insetBy(dx: -10, dy: -10).contains(point)
}SwiftUI:
Button(action: dismiss) {
Image(systemName: "xmark")
.padding(12)
}
.contentShape(Rectangle())Color Contrast
Minimum ratios (WCAG 2.1)
| Text size | Ratio |
|---|---|
| Normal text (<18pt) | 4.5:1 |
| Large text (≥18pt or 14pt bold) | 3:1 |
| Non-text (icons, borders) | 3:1 |
Test contrast
- Accessibility Inspector: Window > Color Contrast Calculator
- Online tools: WebAIM Contrast Checker
High contrast support
Provide alternate colors for Increase Contrast setting:
Asset Catalog: Add High Contrast appearance variants.
UIKit:
if UIAccessibility.isDarkerSystemColorsEnabled {
label.textColor = .label // Higher contrast
}SwiftUI — check the environment:
@Environment(\.colorSchemeContrast) private var contrast
var buttonColor: Color {
contrast == .increased ? .primary : .accentColor
}
var body: some View {
Button(action: action) {
Text(title)
}
.foregroundStyle(buttonColor)
}Semantic system colors (.primary, .secondary) adapt automatically, but use colorSchemeContrast when you need custom behavior.
Don't Rely on Color Alone
Users with color blindness or Differentiate Without Color enabled need additional cues.
Bad
statusLabel.textColor = status == .error ? .red : .greenGood
statusLabel.text = status == .error ? "⚠️ Error" : "✓ Success"
statusLabel.textColor = status == .error ? .systemRed : .systemGreenUse icons, shapes, patterns, or text alongside color.
Avoid Text in Images
Text baked into images is not readable by VoiceOver, not scalable with Dynamic Type, and not localizable.
Use real text whenever possible. If you must use an image that contains text:
- Provide a localized
accessibilityLabel - Replace the image per language when needed
- Prefer vector PDFs so images stay sharp at larger sizes
Media Accessibility
If your app plays audio or video, provide multiple ways to access the content:
- Captions/Subtitles for all spoken content
- SDH (subtitles for the deaf and hard of hearing) when available
- Audio descriptions for visually important content
- Transcripts for long‑form audio/video
Use the system captions preference as the default when possible.
Hearing Accommodations
For users who are deaf or hard of hearing, include alternatives to sound:
- LED Flash for Alerts (system setting) is a common way users notice notifications
- Mono Audio helps users with hearing loss in one ear
- Audio balance lets users favor left/right channels
If your app provides custom audio controls, avoid breaking system preferences.
Check the setting
if UIAccessibility.shouldDifferentiateWithoutColor {
// Add extra visual cues
}Reduce Motion
Users enable Reduce Motion to minimize vestibular triggers.
Honor the setting
UIKit:
if UIAccessibility.isReduceMotionEnabled {
// Use fade instead of slide
// Disable parallax
// Stop auto-playing animations
}SwiftUI — environment value:
@Environment(\.accessibilityReduceMotion) var reduceMotion
.animation(reduceMotion ? nil : .default, value: isExpanded)Check during user interactions
For real-time checks during interactions (sliders, scrubbing), use the UIKit API:
.onChange(of: sliderValue) { _, newValue in
let reduceMotion = UIAccessibility.isReduceMotionEnabled
if !reduceMotion {
// Animate UI updates during scrubbing
withAnimation(.easeInOut(duration: 0.3)) {
updateVisualFeedback(newValue)
}
} else {
// Skip animation, update immediately
updateVisualFeedback(newValue)
}
}Example: Slider with conditional animation
Slider(value: $progress)
.onChange(of: progress) { _, newValue in
if !UIAccessibility.isReduceMotionEnabled {
withAnimation { currentLineIndex = calculateLine(newValue) }
} else {
currentLineIndex = calculateLine(newValue)
}
}Observe changes
NotificationCenter.default.addObserver(
self,
selector: #selector(handleReduceMotion),
name: UIAccessibility.reduceMotionStatusDidChangeNotification,
object: nil
)Reduce Transparency
Simplify translucent backgrounds when Reduce Transparency is enabled:
UIKit:
override func awakeFromNib() {
super.awakeFromNib()
NotificationCenter.default.addObserver(
self,
selector: #selector(updateBackground),
name: UIAccessibility.reduceTransparencyStatusDidChangeNotification,
object: nil
)
updateBackground()
}
@objc private func updateBackground() {
let opacity = UIAccessibility.isReduceTransparencyEnabled ? 1.0 : 0.9
backgroundView.backgroundColor = UIColor.secondarySystemBackground.withAlphaComponent(opacity)
}Note: UIVisualEffectView and system navigation bars already respect Reduce Transparency. Custom alpha on views does not—handle it manually.
SwiftUI:
@Environment(\.accessibilityReduceTransparency) var reduceTransparency
var body: some View {
Text(message)
.background(Color(UIColor.secondarySystemBackground)
.opacity(reduceTransparency ? 1.0 : 0.90))
}Video Playback Preferences
Auto-Play Video Previews
Respect the system Auto-Play preference for motion-heavy previews:
// Use system preference as the default
if UIAccessibility.isVideoAutoplayEnabled {
startPreviewPlayback()
} else {
showStaticThumbnail()
}
// Observe changes
NotificationCenter.default.addObserver(
self,
selector: #selector(handleAutoplayPreferenceChange),
name: UIAccessibility.videoAutoplayStatusDidChangeNotification,
object: nil
)If you also have an in-app preference, use the system setting as the default and let users override it explicitly.
Closed Captions
If your app plays video, honor the system captions preference:
if UIAccessibility.isClosedCaptioningEnabled {
enableClosedCaptions()
}Prefer SDH (Subtitles for the Deaf and Hard of Hearing) when available.
Semantic Colors
Use semantic system colors for better contrast and automatic Light/Dark mode support:
UIKit:
// Instead of:
label.textColor = UIColor.darkGray
// Use:
label.textColor = .secondaryLabel
// Common semantic colors:
// .label - Primary text
// .secondaryLabel - Secondary text (adapts to Dark mode + Increase Contrast)
// .systemBackground - Primary background
// .secondarySystemBackground - Secondary backgroundSwiftUI:
Text("Title")
.foregroundStyle(.primary)
Text("Subtitle")
.foregroundStyle(.secondary)Using semantic colors gives you Light/Dark mode and Increase Contrast support (4 combinations) for free.
Bold Text
When Bold Text is enabled, system fonts become bolder automatically. Custom fonts and non-text elements need manual handling.
UIKit:
if UIAccessibility.isBoldTextEnabled {
label.font = UIFont(name: "Avenir-Heavy", size: 17)
} else {
label.font = UIFont(name: "Avenir-Medium", size: 17)
}For SF Symbols, use weighted variants:
let config = UIImage.SymbolConfiguration(weight: UIAccessibility.isBoldTextEnabled ? .bold : .regular)
imageView.preferredSymbolConfiguration = configSwiftUI — use legibilityWeight environment:
@Environment(\.legibilityWeight) private var legibilityWeight
var fontWeight: Font.Weight {
legibilityWeight == .bold ? .bold : .regular
}
Text("Content")
.fontWeight(fontWeight)Scale non-text elements with Bold Text
Increase border widths, icon weights, and other visual elements:
@Environment(\.legibilityWeight) private var legibilityWeight
@ScaledMetric(relativeTo: .body) private var baseBorderWidth: CGFloat = 2.0
private var borderWidth: CGFloat {
legibilityWeight == .bold ? baseBorderWidth * 2 : baseBorderWidth
}
var body: some View {
content
.overlay(
RoundedRectangle(cornerRadius: 8)
.strokeBorder(.tint, lineWidth: borderWidth)
)
}System fonts adapt automatically — use legibilityWeight for custom visual elements.
Button Shapes
When Button Shapes is enabled, buttons show underlines or borders. Standard buttons handle this automatically; custom buttons may need attention.
UIKit:
if UIAccessibility.buttonShapesEnabled {
// Add visual border or underline to custom buttons
}SwiftUI:
@Environment(\.accessibilityShowButtonShapes) private var showButtonShapes
var body: some View {
Button(action: onTap) {
Text(title)
.padding()
}
.overlay {
if showButtonShapes {
RoundedRectangle(cornerRadius: 8)
.strokeBorder(.primary, lineWidth: 1)
}
}
}Example: Custom list row buttons
struct TranscriptLineView: View {
@Environment(\.accessibilityShowButtonShapes) private var showButtonShapes
var body: some View {
Button(action: onTap) {
content
.background(backgroundColor)
.overlay(buttonShapeOverlay)
}
.buttonStyle(.plain)
}
@ViewBuilder
private var buttonShapeOverlay: some View {
if showButtonShapes {
RoundedRectangle(cornerRadius: 8)
.strokeBorder(.primary, lineWidth: 1)
}
}
}Haptic Feedback
Use haptics to reinforce important events — but never as the only feedback channel.
let generator = UINotificationFeedbackGenerator()
generator.notificationOccurred(.success) // .warning, .errorlet impact = UIImpactFeedbackGenerator(style: .medium)
impact.impactOccurred()Use .success for completions, .warning for caution, .error for failures.
When feedback style matters (for example, rhythm games, timers, coaching cues), give users control over channels (audio, haptic, visual) instead of forcing one output mode.
Keyboard Shortcuts
Provide shortcuts for key actions on iPad and external keyboards.
UIKit:
override var keyCommands: [UIKeyCommand]? {
[
UIKeyCommand(title: "Refresh", action: #selector(refresh), input: "r", modifierFlags: .command),
UIKeyCommand(title: "Search", action: #selector(search), input: "f", modifierFlags: .command)
]
}SwiftUI:
Button("Refresh", action: refresh)
.keyboardShortcut("r", modifiers: .command)Shortcuts appear when the user holds the Command key.
Multi-Modal Information
Convey important information through multiple channels:
| Channel | Example |
|---|---|
| Visual | Error icon |
| Text | "Password is too short" |
| Color | Red text |
| Haptic | Error feedback |
| Sound | Alert tone |
Never rely on a single channel.
Multiple Input Paths
For time-sensitive or precision-heavy flows (for example, games, media controls, drawing tools), avoid forcing one interaction method.
Provide at least two reliable input paths where possible:
- Touch gestures and on-screen controls
- Hardware keyboard shortcuts
- External controllers or alternate navigation patterns
This improves access for users who cannot perform one specific gesture pattern consistently.
Orientation Support
Support both portrait and landscape when possible. Don't force users to rotate their devices.
override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
.all
}Some users mount devices in fixed orientations.
Avoid Ephemeral Feedback
Snackbars and toasts that disappear quickly are problematic:
- VoiceOver users may miss them
- Zoom users can't see them
- Slow readers can't finish them
For critical information, use:
- Persistent banners
- Confirmation dialogs
- Inline error messages
Alt Text for Shared Images
If your app lets users share images, provide a way to add alt text:
let attachment = UIDragItem(itemProvider: provider)
attachment.localObject = ["image": image, "altText": altText]Platforms like Twitter and Slack do this well.
Smart Invert
Prevent images and media from inverting with Smart Invert:
imageView.accessibilityIgnoresInvertColors = true
videoPlayer.accessibilityIgnoresInvertColors = trueInvert Colors (Classic)
Some users enable Invert Colors (Settings > Accessibility > Display & Text Size). If you use custom color combinations, you may want to adjust them when inversion is enabled.
if UIAccessibility.isInvertColorsEnabled {
// Adjust custom colors if needed
contentView.backgroundColor = .systemBackground
}
NotificationCenter.default.addObserver(
self,
selector: #selector(handleInvertColorsChange),
name: UIAccessibility.invertColorsStatusDidChangeNotification,
object: nil
)
@objc private func handleInvertColorsChange() {
// Update custom colors when setting changes
}Avoid relying on color alone—use icons and labels for context, so inversion does not remove meaning.
Toggles and Switches
Group switches with their labels:
UIKit:
// Place switch in table view cell accessory
cell.accessoryView = toggle
cell.accessibilityLabel = "Notifications"
cell.accessibilityValue = toggle.isOn ? "On" : "Off"SwiftUI:
Toggle("Notifications", isOn: $notificationsEnabled)The standard Toggle handles accessibility automatically.
Announce setting changes
When a toggle affects other settings or has side effects, announce the change:
Toggle("Respect Assistive Technology Settings", isOn: $prefersATSettings)
.onChange(of: prefersATSettings) { _, newValue in
// Update related settings
if newValue {
customVoiceEnabled = false
}
// Announce the change
announceChange(newValue
? "Custom voice disabled"
: "Custom voice enabled"
)
}
private func announceChange(_ message: String) {
Task { @MainActor in
// Small delay ensures VoiceOver finishes reading the toggle
try? await Task.sleep(nanoseconds: 100_000_000)
UIAccessibility.post(notification: .announcement, argument: message)
}
}This helps VoiceOver users understand cascading effects of their choices.
Watchables and Wearables
Assistive Touch on Apple Watch
Users can navigate with hand gestures (pinch, clench). If your watch app supports VoiceOver, Assistive Touch likely works too.
Quick actions
Implement a quick action for the most important task:
.accessibilityQuickAction(style: .prompt) {
Button("Play") { play() }
}Triggered with a double-pinch gesture.
Checklist
- [ ] Touch targets at least 44×44 points
- [ ] Color contrast meets minimums (4.5:1 for text)
- [ ] Increase Contrast honored for custom colors
- [ ] Information conveyed in multiple modes (not just color)
- [ ] Reduce Motion honored (including during interactions)
- [ ] Reduce Transparency honored
- [ ] Bold Text supported for custom fonts and borders
- [ ] Button Shapes honored for custom buttons
- [ ] Haptics used for key events
- [ ] Keyboard shortcuts for main actions
- [ ] Both orientations supported
- [ ] Ephemeral messages replaced with persistent alternatives
- [ ] Images/video ignore Smart Invert
- [ ] Toggles grouped with labels
- [ ] Setting changes announced when they have side effects
Sources
- Accessibility Up To 11 — #365DaysIOSAccessibility
- From Zero to Accessible (Daniel Devesa Derksen-Staats and Rob Whitaker)
Accessibility Playbook
Use this playbook for common mistakes, Accessibility Inspector warnings, core patterns, version-specific APIs, and verification checklists. Pair this with framework-specific references (VoiceOver, Dynamic Type, etc.) for deeper guidance.
Contents
- Common Mistakes Playbook
- Common Accessibility Inspector Warnings
- Core Patterns Reference
- Testing Workflow
- iOS Version-Specific APIs
- Common Scenarios → Quick Navigation
- Best Practices Summary
- Verification Checklist (After Making Changes)
- Review Checklist (Quick Check)
- Sources
Common Mistakes Playbook
When you see these patterns (framed as user-experience issues), suggest the fix. Examples show both UIKit and SwiftUI where applicable.
VoiceOver reads nothing or reads "button"
Cause: Element has no label or an empty label. Very common with icon-only buttons. Fix: Add accessibilityLabel with a concise description.
// UIKit — Icon button without label: VoiceOver reads "button"
let closeButton = UIButton(type: .system)
closeButton.setImage(UIImage(systemName: "xmark"), for: .normal)
// Good:
closeButton.accessibilityLabel = "Close"
// SwiftUI — Bad: VoiceOver reads "button"
Button(action: close) { Image(systemName: "xmark") }
// SwiftUI — Good: VoiceOver reads "Close, button"
Button(action: close) { Image(systemName: "xmark") }
.accessibilityLabel("Close")VoiceOver reads each element separately in a cell and navigation is tedious
Cause: Elements not grouped; user swipes through every label and button. Fix: Group the cell as a single element with custom actions for buttons.
// UIKit
cell.isAccessibilityElement = true
cell.accessibilityLabel = "\(title), \(subtitle)"
cell.accessibilityCustomActions = [
UIAccessibilityCustomAction(name: "Add to cart") { _ in self.addToCart(); return true }
]
// SwiftUI — Single swipe; combine children so label is built from inner views' accessibility
HStack {
AsyncImage(url: imageURL)
VStack(alignment: .leading) { Text(title); Text(subtitle) }
}
.accessibilityElement(children: .combine)
// Inner views' labels/values contribute to the group announcement
// SwiftUI — Or single swipe with custom actions
NavigationLink { ... } label: { content }
.accessibilityAction(named: "Add to cart") { addToCart() }VoiceOver reads a custom control as many separate elements or buttons
Cause: Each part (star, thumb, increment/decrement) is a separate focusable element. Fix: Prefer [`.accessibilityRepresentation`](https://developer.apple.com/documentation/swiftui/view/accessibilityrepresentation(representation:)) (iOS 16+) when a similar native control exists. Otherwise group as one element: use adjustable (rating, stepper) when the control has increment/decrement semantics; use custom actions or a single button when it doesn't (e.g. a custom picker with discrete options).
// SwiftUI — Best: use representation if a native control fits (e.g. Stepper)
CustomRatingView(rating: $rating)
.accessibilityRepresentation {
Stepper("Rating", value: $rating, in: 1...5)
}
// SwiftUI — If custom implementation required: single element, adjustable
customControl
.accessibilityElement(children: .ignore)
.accessibilityLabel("Rating")
.accessibilityValue("\(rating) of 5")
.accessibilityAdjustableAction { direction in ... }
// UIKit — Same idea: one element, custom actions or adjustable
view.isAccessibilityElement = true
view.accessibilityLabel = "Rating"
view.accessibilityValue = "\(rating) of 5"
view.accessibilityTraits = .adjustable
// Implement accessibilityIncrement() / accessibilityDecrement()Text doesn't scale with Dynamic Type
Cause: Fixed font size used. Fix: Use text styles.
// UIKit
label.font = UIFont.preferredFont(forTextStyle: .body)
label.adjustsFontForContentSizeCategory = true
// SwiftUI
Text("Content")
.font(.body) // Scales automaticallyLayout breaks at large text sizes
Cause: Horizontal layout can't accommodate larger text. Fix: Use adaptive layout based on dynamicTypeSize (or preferredContentSizeCategory) when you want deterministic behavior across repeated items. Use ViewThatFits when you want a local fallback based on actual fit in a specific part of the layout, or when the fallback is more complex than just flipping stack axis.
// SwiftUI — Adaptive stack: flip axis at accessibility sizes (iOS 16+ [AnyLayout](https://developer.apple.com/documentation/swiftui/anylayout))
@Environment(\.dynamicTypeSize) private var dynamicTypeSize
var body: some View {
let layout = dynamicTypeSize.isAccessibilitySize ? AnyLayout(VStackLayout()) : AnyLayout(HStackLayout())
layout { content }
}
// SwiftUI — Same idea, conditional stack (all iOS versions)
@Environment(\.dynamicTypeSize) var dynamicTypeSize
var body: some View {
if dynamicTypeSize.isAccessibilitySize { VStack { content } } else { HStack { content } }
}
// SwiftUI — Fit-based fallback for a local layout block
ViewThatFits {
HStack { content } // Preferred compact layout
VStack { content } // Fallback when horizontal doesn't fit
}
// UIKit
if traitCollection.preferredContentSizeCategory.isAccessibilityCategory {
stackView.axis = .vertical
} else {
stackView.axis = .horizontal
}Prefer deterministic rules for repeated list/grid items; otherwise different rows may resolve to different layouts depending on content length.
Toast/snackbar disappears before VoiceOver reaches it
Cause: Ephemeral feedback with no announcement. Fix: Post an announcement and consider persistent alternatives.
// UIKit
UIAccessibility.post(notification: .announcement, argument: message)
// SwiftUI
var announcement = AttributedString(message)
announcement.accessibilitySpeechAnnouncementPriority = .high
AccessibilityNotification.Announcement(announcement).post()Voice Control can't find or activate a button
Cause: Label differs from visible text, or Voice Control needs intuitive names. Fix: Voice Control defaults to using the accessibilityLabel for recognition. Provide additional alternatives with accessibilityInputLabels when you need synonyms or shorter commands (e.g., "Remove", "Delete" for "Remove User").
// SwiftUI
Button("Remove User") { remove() }
.accessibilityInputLabels(["Remove User", "Remove", "Delete"])
// UIKit
button.accessibilityLabel = "Remove User"
button.accessibilityUserInputLabels = ["Remove User", "Remove", "Delete"]Custom interactive controls using tap gestures not exposed to assistive tech as buttons
Cause: View uses onTapGesture (SwiftUI) or UITapGestureRecognizer (UIKit) but isn't exposed as a button to VoiceOver. Fix: Prefer a native `Button` (SwiftUI) or `UIButton` (UIKit) for better accessibility out-of-the-box. If you must use a custom view with gestures, make it an accessibility element and add the button trait.
// SwiftUI — Best: use a native Button
Button { select() } label: { HStack { Text("Option") } }
// SwiftUI — If custom view required: add trait and label manually
HStack { Text("Option") }
.onTapGesture { select() }
.accessibilityAddTraits(.isButton)
.accessibilityLabel("Option")
// UIKit — Best: use UIButton
// If custom view required: make it accessible
customView.isAccessibilityElement = true
customView.accessibilityTraits.insert(.button)
customView.accessibilityLabel = "Option name"
customView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(handleTap)))Selection state not conveyed
Cause: Selection indicator (checkmark, highlight, radio button, checkbox) is visual only; VoiceOver doesn't know it's selected. Fix: Add the .selected trait.
// SwiftUI
.accessibilityAddTraits(isSelected ? .isSelected : [])
// UIKit
accessibilityTraits = isSelected ? accessibilityTraits.union(.selected) : accessibilityTraits.subtracting(.selected)VoiceOver users can't navigate by headings
Cause: Section titles or headings aren't marked with the header trait; Headings rotor doesn't list them. Fix: Add the .header trait to section titles and headings.
// SwiftUI
Text("Section Title")
.accessibilityAddTraits(.isHeader)
// UIKit
sectionTitleLabel.accessibilityTraits.insert(.header)Decorative image can be reached with VoiceOver
Cause: Image that doesn't convey meaningful information is still in the accessibility tree; VoiceOver users have to swipe past it. Fix: Hide decorative images from assistive technologies.
// SwiftUI
Image("decoration")
.accessibilityHidden(true)
// UIKit
decorativeImageView.isAccessibilityElement = falseCommon Accessibility Inspector Warnings
When Accessibility Inspector (Xcode > Window > Accessibility Inspector > Audit) reports issues, it provides suggested fixes. The most common warnings overlap with the Common Mistakes Playbook above. Use this section as a quick reference for Inspector-specific guidance.
"Element has no label"
→ See VoiceOver reads nothing or reads "button" above.
"Text doesn't support Dynamic Type"
→ See Text doesn't scale with Dynamic Type above.
"Contrast ratio below 4.5:1" (or 3:1 for large text)
Fix: Use semantic colors or increase contrast.
// UIKit
label.textColor = .label // Adapts to Light/Dark + Increase Contrast
// SwiftUI
Text("Content")
.foregroundStyle(.primary)→ See good-practices.md#color-contrast
"Touch target size below 44x44 points"
Fix: Ensure minimum 44×44 points; allow larger for Dynamic Type.
// UIKit
button.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
button.widthAnchor.constraint(greaterThanOrEqualToConstant: 44),
button.heightAnchor.constraint(greaterThanOrEqualToConstant: 44)
])
// SwiftUI
Button(action: action) {
Image(systemName: "info")
.padding(12)
}
.contentShape(Rectangle())→ See good-practices.md#touch-target-size
"Element has a label but no traits"
→ See Custom interactive controls using tap gestures not exposed to assistive tech as buttons above.
"Element is not accessible"
Fix: Make it an accessibility element.
// UIKit
customView.isAccessibilityElement = true
customView.accessibilityLabel = "Description"
// SwiftUI - usually automatic, but check:
customView
.accessibilityLabel("Description")Core Patterns Reference
Important: Always use localized strings for accessibility labels, values, and hints. Match your project's localization patterns (e.g., NSLocalizedString("close_button", comment: "") in UIKit, or Text("close_button") with .xcstrings in SwiftUI).
When to Use Each Property
accessibilityLabel — Name of the element
// UIKit
closeButton.accessibilityLabel = "Close"
// SwiftUI
Button("") { close() }
.accessibilityLabel("Close")accessibilityValue — Current state
// UIKit
slider.accessibilityValue = "50 percent"
// SwiftUI
Slider(value: $value, in: 0...100)
.accessibilityValue("\(Int(value)) percent")accessibilityHint — Extra context (use sparingly; only for non-obvious actions)
// UIKit
deleteButton.accessibilityHint = "Removes the item from your list"
// SwiftUI
Button("Delete") { delete() }
.accessibilityHint("Removes the item from your list")accessibilityTraits — Role and state
// UIKit
sectionTitle.accessibilityTraits.insert(.header)
// SwiftUI
Text("Section Title")
.accessibilityAddTraits(.isHeader)Common Patterns (UIKit)
Grouping elements:
cardView.isAccessibilityElement = true
cardView.accessibilityLabel = "\(title), \(subtitle)"
cardView.accessibilityTraits = .buttonCustom actions:
cell.accessibilityCustomActions = [
UIAccessibilityCustomAction(name: "Delete") { _ in
self.delete()
return true
}
]Adjustable controls:
// For controls with increment/decrement (sliders, steppers, etc.)
customControl.accessibilityTraits = .adjustable
customControl.accessibilityLabel = "Volume"
customControl.accessibilityValue = "\(volume)%"
// Override these methods
override func accessibilityIncrement() {
volume = min(volume + 10, 100)
accessibilityValue = "\(volume)%"
}
override func accessibilityDecrement() {
volume = max(volume - 10, 0)
accessibilityValue = "\(volume)%"
}Moving focus:
UIAccessibility.post(notification: .layoutChanged, argument: errorLabel)Common Patterns (SwiftUI)
Grouping elements:
HStack {
Image(systemName: "star.fill")
Text("Favorite")
}
.accessibilityElement(children: .combine)Custom actions:
.accessibilityAction(named: "Delete") {
deleteItem()
}Adjustable controls:
.accessibilityAdjustableAction { direction in
switch direction {
case .increment: value += 1
case .decrement: value -= 1
@unknown default: break
}
}Moving focus (iOS 15+ with [`AccessibilityFocusState`](https://developer.apple.com/documentation/swiftui/accessibilityfocusstate)):
@AccessibilityFocusState private var isFocused: Bool
// Move focus to element
Button("Submit") { submit() }
.accessibilityFocused($isFocused)
// Trigger focus
isFocused = trueTesting Workflow
Canonical testing guidance lives in:
testing-manual.mdfor assistive-technology and settings workflowstesting-automated.mdfor audits, UI tests, and CI guardrails
Use this quick sequence in day-to-day development: 1. During development: Run Accessibility Inspector checks. 2. Before PR: Validate VoiceOver and Dynamic Type end-to-end on key flows. 3. Regression prevention: Add or update automated checks where they provide signal. 4. Before release: Run the full manual testing checklist. 5. Continuous improvement: Include feedback from users with disabilities when possible.
iOS Version-Specific APIs
Some accessibility features require specific iOS versions. Check deployment target before recommending these APIs; provide fallbacks when possible.
iOS 13+
- Large Content Viewer (`UILargeContentViewerItem`), `UILargeContentViewerInteraction`
- SF Symbols
- `preferredContentSizeCategory.isAccessibilityCategory`
iOS 14+
- Switch Control custom action images (`UIAccessibilityCustomAction.init(name:image:actionHandler:)`))
iOS 15+
- `AccessibilityFocusState` for programmatic focus management (SwiftUI)
- `.accessibilityRotor`) for custom rotors (SwiftUI)
iOS 16+
- `.accessibilityRepresentation`) for custom control alternatives (SwiftUI)
- `.accessibilityActions { }` syntax)
iOS 17+
- `.sensoryFeedback()`) for haptic responses
Common Scenarios → Quick Navigation
Use this table to quickly find solutions for specific problems (framed as user-experience issues):
| Scenario | Common Mistakes Section | Reference File |
|---|---|---|
| Button doesn't work with VoiceOver | VoiceOver reads nothing or "button" | voiceover-*.md |
| Cell requires many swipes | VoiceOver reads each element separately | voiceover-*.md (Grouping) |
| Custom control reads many "button"s | VoiceOver reads a custom control as many separate elements | voiceover-*.md (Adjustable) |
| Text truncates at large sizes | Text doesn't scale with Dynamic Type | dynamic-type-*.md |
| Layout breaks at large text | Layout breaks at large text sizes | dynamic-type-*.md (Adaptation) |
| Toast disappears too fast | Toast/snackbar disappears before VoiceOver | voiceover-*.md (Announcements) |
| Voice Control can't find button | Voice Control can't find or activate button | voice-control.md |
| Images wrong with Smart Invert | Images look wrong with Smart Invert | good-practices.md |
| Custom view/button not activatable | VoiceOver can't activate custom view | voiceover-*.md (Traits) |
| Selection state not conveyed | Selection state not conveyed | voiceover-*.md (Selected trait) |
| VoiceOver can't navigate by headings | VoiceOver users can't navigate by headings | voiceover-*.md (Headers) |
| Decorative image in VoiceOver order | Decorative image can be reached with VoiceOver | voiceover-*.md (Hidden) |
Best Practices Summary
Goal: The app should support assistive technologies without losing any content or functionality. Use the Common Mistakes Playbook and Anti-Patterns section together with this list.
1. Label everything interactive — Every button, control, and image needs a label or must be hidden 2. Use traits correctly — Traits communicate role; don't rely on labels alone 3. Group related content — Reduce swipe count and cognitive load 4. Support Dynamic Type — Use text styles, not fixed sizes 5. Test with assistive tech — Automation catches basics; manual testing finds real issues 6. Move focus appropriately — After navigation or errors, move VoiceOver focus 7. Provide alternatives — Custom gestures need accessible fallbacks 8. Honor user settings — Respect Reduce Motion, Increase Contrast, Bold Text 9. Think multi-modal — Don't rely on color alone; use icons, text, haptics 10. Iterate with users — Validate with real feedback and keep refining
Verification Checklist (After Making Changes)
For Agents (Automated Checks)
Use these checks when suggesting or applying accessibility changes:
- [ ] Build succeeds with no new warnings
- [ ] Run existing unit tests; no new failures
- [ ] No breaking API changes for the project's deployment target
- [ ] APIs used match the project's iOS version (see Project Capabilities)
- [ ] Pattern consistency: UIKit vs SwiftUI matches the file being edited; labels/traits follow existing project style
- [ ] Linter / static analysis shows no new errors
For Developers (Manual Testing)
Use this checklist when verifying changes before PR or release. Agents can suggest these steps; developers perform them.
- [ ] Accessibility Inspector (Xcode > Window > Accessibility Inspector > Audit)
- Color contrast ratios (≥4.5:1 for text, ≥3:1 for UI elements)
- Touch target sizes (≥44×44 points)
- All interactive elements have labels
- [ ] VoiceOver: Test end-to-end with VoiceOver enabled (on device when possible; consider Screen Curtain)
- Navigate through the changed views
- Verify labels, values, traits are correct
- Test custom actions via Actions rotor
- Test header navigation via Headings rotor
- Confirm focus moves appropriately after state changes
- [ ] Dynamic Type: Test with largest accessibility size (Accessibility 5)
- Text doesn't truncate
- Layout adapts (horizontal → vertical if needed)
- No loss of content or functionality
- [ ] Voice Control: Test key flows with Voice Control
- Say "Show names" and verify labels appear
- Say "Tap [element name]" for main interactive elements
- [ ] Full Keyboard Access (if applicable): Test keyboard navigation
- Tab through all interactive elements
- All interactive elements can be reached
- Focus order is logical
- [ ] User settings: Reduce Motion, Increase Contrast, Bold Text, Button Shapes, Dark Mode — test that the app adapts and contrast remains sufficient
- [ ] Documentation: Update accessibility identifiers if changed (for UI tests); note manual testing requirements for QA; add code comments for workarounds or compromises
Review Checklist (Quick Check)
Labels and Traits
- [ ] All interactive elements have labels
- [ ] Labels are concise and don't include control type
- [ ] Traits match the role
- [ ] State changes update values (and traits when relevant, e.g. selected)
Structure
- [ ] Related elements grouped
- [ ] Decorative elements hidden
- [ ] Navigation order is logical
- [ ] Focus moves after state changes
Dynamic Type
- [ ] Text scales with system size
- [ ] Layout adapts for large sizes
- [ ] No truncation (unless intentional)
Testing
- [ ] VoiceOver tested end-to-end
- [ ] Dynamic Type tested at accessibility sizes
- [ ] Voice Control can activate all buttons
- [ ] Keyboard navigation works
Sources
Resources and Attribution
Source links and attributions for this skill.
Primary Sources
Accessibility Up To 11
- Blog: Accessibility Up To 11 Blog
- Website: #365DaysIOSAccessibility
Main resource used to develop this Agent Skill. A year-long series of daily iOS accessibility tips by Daniel Devesa Derksen-Staats.
Blog and resources about mobile accessibility by Rob Whitaker. Used with attribution.
Developing Accessible iOS Apps (Apress)
- Book: Developing Accessible iOS Apps
- Source code: Apress sample code
Comprehensive guide to iOS accessibility by Daniel Devesa Derksen-Staats.
Workshop Materials
Workshop exercises for learning iOS accessibility. Contains the same coffee shop app implemented in both UIKit and SwiftUI, with "before" (inaccessible) and "after" (accessible) versions demonstrating real-world accessibility fixes. Attributed to Daniel Devesa Derksen-Staats and Rob Whitaker.
Fostering an Accessibility Culture
- Article: Fostering an Accessibility Culture
Guidance on building accessibility culture in organizations by Daniel Devesa Derksen-Staats.
Attribution Notes
- Examples derived from
fromZeroToAccessibleare attributed to Rob Whitaker and Daniel Devesa Derksen-Staats.
Additional Resources
Apple Documentation
- Human Interface Guidelines: Accessibility
- UIKit Accessibility
- SwiftUI Accessibility
- Overview of Accessibility Nutrition Labels
- Manage Accessibility Nutrition Labels
Complementary Accessibility Skills
Both of the following skills were released before this one.
- swift-accessibility-skill by Pasquale Vittoriosi — stronger coverage for topics outside this skill's core iOS scope (for example: macOS accessibility, Accessibility Nutrition Labels, and WCAG)
- apple-accessibility-skills by Roberto Gómez — complementary accessibility guidance for Apple-platform development
Agent Skills Inspiration
- Agent Skills: Replacing AGENTS.md with reusable AI knowledge by Antoine van der Lee
- SwiftUI-Agent-Skill by Antoine van der Lee
WWDC Sessions
Search for "accessibility" on developer.apple.com/videos:
- Writing Great Accessibility Labels (2019)
- Make Your App Visually Accessible (2020)
- SwiftUI Accessibility: Beyond the Basics (2021)
- Create accessible spatial experiences (2023)
Community
- Mobile A11y
- Accessible Mobile Apps newsletter by Robin Kanatzar
- Create with Swift — Make It Accessible
- BBC Mobile Accessibility Guidelines
- Global Accessibility Awareness Day
- Accessibility Up To 11 — Resources
Tools
- Accessibility Inspector (built into Xcode)
Exemplary Apps
Examples demonstrating exceptional accessibility:
The Art of Fauna (2025 Apple Design Award - Inclusivity)
- Key patterns: Dual interaction modes (picture/text puzzles), content filters for phobias, dyslexia-friendly fonts
- Lesson: Games are among the hardest software to make accessible. Made by an indie developer with limited resources, this proves accessibility is about willingness, not budget.
- Link: The Art of Fauna
Switch Control
Switch Control enables users with motor impairments to navigate iOS using external switches, head movements, or other adaptive hardware.
How It Works
Switch Control scans through elements one at a time. When the desired element is highlighted, the user activates a switch to select it.
Scanning modes
| Mode | Behavior |
|---|---|
| Auto Scan | Cursor moves automatically through elements |
| Manual Scan | User triggers each move |
| Step Scan | Multi-switch navigation (next/previous/select) |
Enable Switch Control
Settings > Accessibility > Switch Control
Impact on Development
Good VoiceOver support generally means good Switch Control support. The same accessibility properties apply:
- Labels
- Traits
- Grouping
- Custom actions
Reduce Scanning Steps
Group related elements
Fewer elements means fewer scan steps to reach the target.
UIKit:
containerView.isAccessibilityElement = true
containerView.accessibilityLabel = "\(title), \(subtitle)"SwiftUI:
HStack {
Image(systemName: "star.fill")
Text("Favorite")
}
.accessibilityElement(children: .combine)Semantic grouping
Use container types to organize controls:
toolbar.accessibilityContainerType = .semanticGroupSwitch Control recognizes groups and offers "Scan this group" actions.
Custom Actions
Switch Control surfaces custom actions through its menu system.
cell.accessibilityCustomActions = [
UIAccessibilityCustomAction(
name: "Delete",
image: UIImage(systemName: "trash"),
actionHandler: { _ in
self.delete()
return true
}
),
UIAccessibilityCustomAction(
name: "Share",
image: UIImage(systemName: "square.and.arrow.up"),
actionHandler: { _ in
self.share()
return true
}
)
]Images appear in the Switch Control menu (iOS 14+) with `UIAccessibilityCustomAction.init(name:image:actionHandler:)`).
Traits
Ensure traits are correct:
.buttonfor tappable elements.selectedfor current selection.notEnabledfor disabled controls.adjustablefor steppers and sliders
Missing traits confuse navigation.
Testing
The easiest low-friction option is to use a Bluetooth keyboard:
1. Go to Settings > Accessibility > Switch Control 2. Add a switch, choose Keyboard as the source 3. Map the Space bar to "Select Item" — one key scans; pressing selects 4. Or configure two keys: one for "Move to Next Item", another for "Select Item"
If you don't have a Bluetooth keyboard, you can test using head movements:
- Settings > Accessibility > Switch Control > Switches: add a switch using Left Head Movement (move to next) and Right Head Movement (select)
- Note: enabling this disables other screen touches, so keep the Accessibility Shortcut handy to exit
Once enabled: 1. Let Switch Control scan through your app 2. Verify all elements are reachable and in a logical order 3. Confirm custom actions appear in the menu (reduces step count) 4. Check that grouping reduces unnecessary scan steps
What to verify
- All interactive elements are reachable
- Grouping reduces unnecessary scan steps
- Custom actions are discoverable (complex actions appear in the Switch Control menu rather than requiring manual navigation)
- Traits match element behavior
visionOS
Switch Control is supported in visionOS. Building accessible spatial experiences benefits from the same patterns:
- Clear labels
- Logical grouping
- Exposed actions
Common Issues
| Problem | Solution |
|---|---|
| Too many scan steps | Group related elements |
| Action hidden behind gesture | Add custom action |
| Element unreachable | Set isAccessibilityElement = true |
| Trait missing | Add appropriate traits |
Checklist
- [ ] Elements grouped to reduce scan steps
- [ ] Custom actions provided for secondary features
- [ ] Traits match behavior
- [ ] Tested with Switch Control enabled
Sources
- https://accessibilityupto11.com/365-days-ios-accessibility/
- https://accessibilityupto11.com/blog/
Automated Testing
Automated accessibility testing for iOS: UI tests, static analysis, and Accessibility Inspector.
What Automation Can Do
Automated tools are good for:
- Detecting missing labels
- Catching regressions after code changes
- Enforcing baseline rules
- Flagging obvious issues (contrast, target size)
What Automation Cannot Do
Automation cannot evaluate:
- Whether a label makes sense in context
- If navigation order is logical
- Whether the experience is actually usable
- How complex interactions feel
Always pair automation with manual testing.
UI Testing for Accessibility
Use accessibilityIdentifier for tests
accessibilityIdentifier is for test automation. accessibilityLabel is read by VoiceOver.
// In production code
submitButton.accessibilityIdentifier = "submit-button"
submitButton.accessibilityLabel = "Submit order"// In UI test
let submitButton = app.buttons["submit-button"]
XCTAssertTrue(submitButton.exists)Assert on accessibility properties
let cell = app.cells["order-cell"]
XCTAssertEqual(cell.label, "Order #1234, $42.00")
XCTAssertTrue(cell.accessibilityTraits.contains(.button))Test VoiceOver experience programmatically
Query the accessibility tree:
let app = XCUIApplication()
let elements = app.descendants(matching: .any).allElementsBoundByAccessibilityElement
for element in elements {
if element.isHittable && element.label.isEmpty {
XCTFail("Unlabeled element: \(element)")
}
}Test Dynamic Type
Launch with accessibility size:
let app = XCUIApplication()
app.launchArguments += ["-UIPreferredContentSizeCategoryName", "UICTContentSizeCategoryAccessibilityExtraExtraLarge"]
app.launch()Accessibility Inspector Audit
Launch
Xcode > Open Developer Tool > Accessibility Inspector
Run audit
1. Select target (simulator or device) 2. Click the Audit tab 3. Click "Run Audit"
Common audit findings
| Issue | Fix |
|---|---|
| Missing label | Add accessibilityLabel |
| Insufficient contrast | Increase color contrast |
| Small touch target | Expand hit area to 44×44 |
| Missing traits | Add appropriate traits |
| Image label is file name | Provide a meaningful label or hide decorative images |
Per-element inspection
1. Click the crosshair button 2. Click an element in the simulator 3. View its accessibility properties
VoiceOver preview
Use the speaker icon to hear how VoiceOver would read the current screen. You can step through elements or play all.
Color Contrast Calculator
In Accessibility Inspector: Window > Color Contrast Calculator for quick contrast checks.
Notifications log
Window > Show Notifications to see posted accessibility notifications.
SwiftUI Accessibility Inspector
In Xcode's Inspectors panel (right sidebar), the Accessibility section shows:
- Label
- Value
- Traits
- Identifier
Select a view in the canvas to see its accessibility info.
SwiftLint Rules
Add lint rules to catch common issues:
# .swiftlint.yml
custom_rules:
image_accessibility:
regex: 'Image\s*\(\s*\"[^\"]+\"\s*\)'
message: "Image should have accessibilityLabel or use Image(decorative:)"Community rules also exist for accessibility enforcement.
Automated Contrast Checking
Accessibility Inspector
Window > Show Color Contrast Calculator
Enter foreground and background colors to check ratio.
Programmatic checking
extension UIColor {
func contrastRatio(with other: UIColor) -> CGFloat {
let l1 = relativeLuminance
let l2 = other.relativeLuminance
let lighter = max(l1, l2)
let darker = min(l1, l2)
return (lighter + 0.05) / (darker + 0.05)
}
var relativeLuminance: CGFloat {
var r: CGFloat = 0, g: CGFloat = 0, b: CGFloat = 0
getRed(&r, green: &g, blue: &b, alpha: nil)
func adjust(_ c: CGFloat) -> CGFloat {
c <= 0.03928 ? c / 12.92 : pow((c + 0.055) / 1.055, 2.4)
}
return 0.2126 * adjust(r) + 0.7152 * adjust(g) + 0.0722 * adjust(b)
}
}Unit test for contrast
func testButtonContrastMeetsMinimum() {
let foreground = UIColor.white
let background = UIColor.systemBlue
let ratio = foreground.contrastRatio(with: background)
XCTAssertGreaterThanOrEqual(ratio, 4.5, "Text contrast should be at least 4.5:1")
}Environment Overrides
In Xcode's Debug Area toolbar, click the Environment Overrides button to test:
- Dynamic Type sizes
- Increase Contrast
- Reduce Motion
- Reduce Transparency
- Bold Text
Changes apply immediately without rebuilding.
Continuous Integration
Run UI tests in CI
xcodebuild test \
-scheme MyApp \
-destination 'platform=iOS Simulator,name=iPhone 15' \
-testPlan AccessibilityTestsAccessibility test plan
Create a dedicated test plan for accessibility checks. Run it on every PR.
Limitations
| Tool | Coverage |
|---|---|
| Accessibility Inspector Audit | ~30% of issues |
| UI tests | Regressions, presence of labels |
| SwiftLint | Code patterns only |
The remaining 70%+ requires manual testing.
Checklist
- [ ]
accessibilityIdentifierused for test automation - [ ]
accessibilityLabelassertions in UI tests - [ ] Accessibility Inspector audit passes
- [ ] Contrast checked for custom colors
- [ ] Environment Overrides tested in debug
- [ ] Manual testing performed
Sources
- https://accessibilityupto11.com/365-days-ios-accessibility/
Manual Testing
Testing iOS accessibility with assistive technologies and device settings.
Why Manual Testing
Automated tools catch basic issues but cannot evaluate:
- Whether labels make sense in context
- If navigation order follows a logical flow
- Whether the experience is actually usable
- How complex interactions feel with assistive tech
Always combine automated checks with hands-on testing.
Quick Setup
Accessibility Shortcut
Triple-click the side (or home) button to toggle an assistive technology.
Configure in Settings > Accessibility > Accessibility Shortcut.
Recommended: Add VoiceOver, Voice Control, and Full Keyboard Access.
Tip: choose a small set of shortcuts so you can toggle quickly without cycling through too many options.
Control Center
Add Accessibility Shortcuts to Control Center for faster toggling.
Settings > Control Center > Customize Controls > Accessibility Shortcuts
Siri
- "Turn on VoiceOver"
- "Turn off VoiceOver"
- "Turn on Voice Control"
VoiceOver Testing
Core gestures
| Gesture | Action |
|---|---|
| Tap | Select element |
| Double-tap | Activate |
| Swipe right | Next element |
| Swipe left | Previous element |
| Two-finger rotate | Change rotor mode |
| Swipe up/down | Adjust (if adjustable) or navigate by rotor |
| Two-finger double-tap | Magic Tap |
| Two-finger scrub (Z shape) | Escape |
| Three-finger swipe | Scroll |
Screen Curtain
Triple-tap with three fingers to black out the screen while VoiceOver remains active. Forces non-visual testing.
Use Screen Curtain to validate that core flows work without visual cues. If it feels hard with Screen Curtain, the UI likely needs simplification or better structure.
Caption Panel
Enable in Settings > Accessibility > VoiceOver > Caption Panel to see VoiceOver output at the bottom of the screen.
What to verify
1. Every interactive element is reachable by swiping 2. Labels describe the element clearly 3. Traits match the role (button, header, etc.) 4. Navigation order follows the task flow 5. Focus moves to new content after state changes 6. Errors are announced 7. Custom gestures have accessible alternatives
Advanced gestures
| Gesture | Action |
|---|---|
| Single tap with two fingers | Pause/resume speech |
| Double-tap with three fingers | Mute/unmute VoiceOver speech |
| Triple-tap with one finger | Long press |
| Four-finger tap at top | First element |
| Four-finger tap at bottom | Last element |
Voice Control Testing
Enable
Settings > Accessibility > Voice Control
Or say "Turn on Voice Control" to Siri.
Testing
1. Say "Show names" to overlay accessibility labels 2. Try speaking button labels to activate them 3. Say "Show grid" for precise tapping 4. Say "Show actions" on an element to see custom actions
What to verify
1. Labels match visible text (or are intuitive) 2. No duplicate labels for different controls 3. All interactive elements can be activated by voice 4. Custom actions are discoverable
Full Keyboard Access Testing
Enable
Settings > Accessibility > Keyboards > Full Keyboard Access
Navigation
| Key | Action |
|---|---|
| Tab | Next element |
| Shift + Tab | Previous element |
| Space | Activate |
| Escape | Dismiss |
| Tab + Z | Show actions |
What to verify
1. All interactive elements are focusable 2. Focus order is logical 3. Focus indicator is visible 4. Actions can be triggered from keyboard 5. Keyboard shortcuts are documented and discoverable
Simulator testing
Enable Full Keyboard Access in the simulator. Use your Mac keyboard to navigate.
Switch Control Testing
Enable
Settings > Accessibility > Switch Control
What to verify
1. All elements are reachable via scanning 2. Grouped elements reduce scan steps 3. Custom actions appear in the menu
Zoom Testing
Enable
Settings > Accessibility > Zoom
Gestures
| Gesture | Action |
|---|---|
| Double-tap with three fingers | Toggle zoom |
| Double-tap with three fingers + drag | Adjust zoom level |
| Three-finger drag | Pan while zoomed |
What to verify
1. Content remains usable when zoomed 2. Actions don't trigger outside the viewport 3. Ephemeral feedback (toasts) is visible
Magnifier Testing
Magnifier uses the camera to zoom real-world content. If your app helps users capture or read text, consider:
1. Whether your UI works alongside Magnifier 2. Avoiding critical UI elements in areas often covered by the camera preview
Guided Access Testing
Guided Access locks the device to a single app and can restrict touch areas. If your app is used in education, kiosks, or focused tasks:
1. Ensure the core flow works without system gestures 2. Avoid reliance on multitasking gestures for critical actions 3. If your UI depends on specific screen regions, verify they are not restricted in Guided Access settings 4. Consider time‑limit scenarios if the app is used for timed activities
Dynamic Type Testing
Simulator shortcut
Option + Command + + or - to increase/decrease text size.
Environment Overrides
In Xcode's Debug Area toolbar, click the Environment Overrides button to change Dynamic Type without leaving the debugger.
Use this panel to simulate:
- Dark Mode
- Increase Contrast
- Reduce Motion
- Reduce Transparency
- Bold Text
- Button Shapes
- Grayscale
- Differentiate Without Color
Accessibility Inspector
Xcode > Open Developer Tool > Accessibility Inspector
Use the Settings tab to change text size on a running simulator or device.
SwiftUI variants preview
Preview all sizes at once in Xcode's canvas.
What to verify
1. Text scales with system size 2. No truncation at large sizes (unless intentional) 3. Layout adapts for accessibility sizes 4. Multiline text is readable
Double-length pseudolanguage
Edit scheme > Options > App Language > Double-Length Pseudolanguage
Stress-tests layout with longer strings.
Reduce Motion Testing
Enable
Settings > Accessibility > Motion > Reduce Motion
What to verify
1. Parallax effects are disabled 2. Slide animations become fades 3. Auto-playing animations stop or slow down
Increase Contrast Testing
Enable
Settings > Accessibility > Display & Text Size > Increase Contrast
What to verify
1. Text contrast improves 2. Asset variants (if provided) are used 3. No elements become unreadable
Testing on Device vs Simulator
| Feature | Device | Simulator |
|---|---|---|
| VoiceOver | Full support | Limited (Mac VoiceOver) |
| Voice Control | Full support | Not supported |
| Full Keyboard Access | Full support | Full support |
| Switch Control | Full support | Limited |
| Dynamic Type | Full support | Full support |
| Haptics | Full support | Not available |
Recommendation: Test on device for VoiceOver and Voice Control. Simulator is fine for layout and Dynamic Type.
Accessibility Inspector
Launch
Xcode > Open Developer Tool > Accessibility Inspector
Features
| Tab | Purpose |
|---|---|
| Inspection | View accessibility properties of any element |
| Audit | Run automated checks for common issues |
| Settings | Change Dynamic Type, Reduce Motion, etc. |
Connect to device
Accessibility Inspector works with simulators and physical devices. Useful for inspecting other apps.
Notifications log
Window > Show Notifications to see accessibility notifications (announcements, focus changes).
Testing Checklist
VoiceOver
- [ ] All elements reachable
- [ ] Labels are clear
- [ ] Traits are correct
- [ ] Order is logical
- [ ] Focus moves after state changes
- [ ] Errors are announced
Dynamic Type
- [ ] Text scales
- [ ] No truncation
- [ ] Layout adapts for large sizes
Other
- [ ] Reduce Motion honored
- [ ] Increase Contrast works
- [ ] Voice Control can activate all buttons
- [ ] Full Keyboard Access navigates everything
Tools and Apps
- Accessibility Inspector: Built into Xcode
- ScreenReader app by @JanJaapdeGroot: Learn VoiceOver gestures
- Voice Control "Show names": Overlay labels
Sources
- https://accessibilityupto11.com/365-days-ios-accessibility/
- https://github.com/Apress/developing-accessible-iOS-apps
Voice Control
Voice Control allows users to navigate and interact using only their voice.
How It Works
Voice Control recognizes accessibility labels and lets users speak them to activate controls.
User: "Tap Settings"
→ Activates button with accessibilityLabel "Settings"Voice Control can work offline and relies heavily on the labels you provide. Clear, concise labels lead to faster, more reliable activation.
Enable Voice Control
Settings > Accessibility > Voice Control
Or say "Turn on Voice Control" to Siri.
Key Commands
| Command | Action |
|---|---|
| "Show names" | Overlay labels on all elements |
| "Show numbers" | Overlay numbers on all elements |
| "Tap [label]" | Activate element by name |
| "Tap [number]" | Activate numbered element |
| "Show grid" | Display a grid for precise tapping |
| "Show actions" | Show custom actions for focused element |
| "Scroll down/up" | Scroll the screen |
| "Go back" | Navigate back |
Label Best Practices
Match visible text
If a button shows "Submit", the accessibility label should be "Submit" — not "Send" or "Submit button".
// Button shows "Settings"
settingsButton.accessibilityLabel = "Settings" // ✓ MatchesAvoid duplicates
Multiple elements with the same label cause ambiguity. Voice Control falls back to showing numbers.
Use input labels for alternatives
When the visible text doesn't match what users might say:
UIKit:
gearButton.accessibilityLabel = "Settings"
gearButton.accessibilityUserInputLabels = ["Settings", "Preferences", "Options", "Gear", "Cog"]SwiftUI:
Button(action: openSettings) {
Image(systemName: "gear")
}
.accessibilityLabel("Settings")
.accessibilityInputLabels(["Settings", "Preferences", "Options", "Gear", "Cog"])Users can say any of these alternatives.
Testing with Voice Control
"Show names"
Reveals all accessibility labels overlaid on the screen. Quickly identifies:
- Missing labels (no overlay appears)
- Duplicate labels (same text on multiple elements)
- Confusing labels (text that doesn't match the visual)
Use this during development to validate the labels users will actually speak.
"Show actions"
Focus on an element and say "Show actions" to see custom accessibility actions.
Custom Actions
Voice Control exposes custom actions. Users can say "Show actions for [element]" then activate by name.
cell.accessibilityCustomActions = [
UIAccessibilityCustomAction(name: "Delete", actionHandler: { _ in
self.delete()
return true
})
]User: "Show actions for Message" → "Delete" appears User: "Tap Delete"
Accessibility Values
Values improve Voice Control usability for stateful controls:
slider.accessibilityLabel = "Volume"
slider.accessibilityValue = "50 percent"User: "What is Volume?" → Voice Control announces "50 percent"
Common Issues
| Problem | Solution |
|---|---|
| Element has no label | Add accessibilityLabel |
| Label doesn't match visible text | Update label or use input labels |
| Multiple elements with same label | Make labels unique or use context |
| Icon-only button | Add descriptive label |
| Custom gesture required | Provide accessible alternative |
Checklist
- [ ] Labels match visible text when possible
- [ ] No duplicate labels for different controls
- [ ] Icon-only buttons have descriptive labels
- [ ] Input labels provided for non-obvious names
- [ ] Custom actions exposed for secondary features
- [ ] Tested with "Show names" command
Sources
- https://accessibilityupto11.com/365-days-ios-accessibility/
- https://accessibilityupto11.com/blog/
Related skills
FAQ
Which assistive technologies does ios-accessibility cover?
ios-accessibility addresses VoiceOver, Dynamic Type, Switch Control, Voice Control, and Full Keyboard Access, plus accessibility labels, traits, hints, values, and inclusive design practices for iOS.
Does ios-accessibility support automated testing?
ios-accessibility includes guidance for automated accessibility testing and auditing alongside manual VoiceOver walkthroughs when developers validate UIKit or SwiftUI screens.