axiom-accessibility-diag by charleswiltgen/axiom
npx skills add https://github.com/charleswiltgen/axiom --skill axiom-accessibility-diag针对 iOS/macOS 应用的系统性无障碍功能诊断与修复。涵盖导致 App Store 审核被拒和用户投诉的 7 个最常见无障碍问题。
核心原则 无障碍功能不是可选项。iOS 应用必须支持 VoiceOver、动态类型和足够的色彩对比度才能通过 App Store 审核。残障用户依赖这些功能。
问题 缺少或通用的无障碍标签会阻止 VoiceOver 用户理解 UI 的用途。
WCAG 4.1.2 名称、角色、值(A 级)
// ❌ 错误 - 无标签(VoiceOver 会说“按钮”)
Button(action: addToCart) {
Image(systemName: "cart.badge.plus")
}
// ❌ 错误 - 通用标签
.accessibilityLabel("Button")
// ❌ 错误 - 读取实现细节
.accessibilityLabel("cart.badge.plus") // VoiceOver: "cart dot badge dot plus"
// ✅ 正确 - 描述性标签
Button(action: addToCart) {
Image(systemName: "cart.badge.plus")
}
.accessibilityLabel("Add to cart")
// ✅ 正确 - 为复杂操作添加提示
.accessibilityLabel("Add to cart")
.accessibilityHint("Double-tap to add this item to your shopping cart")
广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
// ✅ 正确 - 对 VoiceOver 隐藏装饰性图像
Image("decorative-pattern")
.accessibilityHidden(true)
// ✅ 正确 - 将多个元素合并为一个标签
HStack {
Image(systemName: "star.fill")
Text("4.5")
Text("(234 reviews)")
}
.accessibilityElement(children: .combine)
.accessibilityLabel("Rating: 4.5 stars from 234 reviews")
问题 固定字体大小会阻止视力障碍用户阅读文本。
WCAG 1.4.4 调整文本大小(AA 级 - 支持 200% 缩放而不丢失内容/功能)
// ❌ 错误 - 固定大小,不会缩放
Text("Price: $19.99")
.font(.system(size: 17))
UILabel().font = UIFont.systemFont(ofSize: 17)
// ❌ 错误 - 自定义字体无缩放
Text("Headline")
.font(Font.custom("CustomFont", size: 24))
// ✅ 正确 - SwiftUI 语义样式(自动缩放)
Text("Price: $19.99")
.font(.body)
Text("Headline")
.font(.headline)
// ✅ 正确 - UIKit 语义样式
label.font = UIFont.preferredFont(forTextStyle: .body)
// ✅ 正确 - 带缩放的自定义字体
let customFont = UIFont(name: "CustomFont", size: 24)!
label.font = UIFontMetrics.default.scaledFont(for: customFont)
label.adjustsFontForContentSizeCategory = true
// ❌ 错误 - 固定大小,不会缩放
Text("Price: $19.99")
.font(.system(size: 17))
// ⚠️ 可接受 - 自定义字体无缩放(无障碍违规)
Text("Headline")
.font(Font.custom("CustomFont", size: 24))
// ✅ 良好 - 随动态类型缩放的自定义大小
Text("Large Title")
.font(.system(size: 60).relativeTo(.largeTitle))
Text("Custom Headline")
.font(.system(size: 24).relativeTo(.title2))
// ✅ 最佳 - 尽可能使用语义样式
Text("Headline")
.font(.headline)
relativeTo: 的工作原理
.title2、.largeTitle 等)示例
.title2 基准:~22pt → 您的自定义:24pt(大 1.09 倍).title2 增长到 ~28pt → 您的自定义增长到 ~30.5pt(保持 1.09 倍比例)修复层次结构(从最佳到最差)
.title、.body、.caption).system(size:).relativeTo().dynamicTypeSize() 修饰符的自定义字体.largeTitle - 34pt(在无障碍尺寸下缩放至 44pt).title - 28pt.title2 - 22pt.title3 - 20pt.headline - 17pt 半粗体.body - 17pt(默认).callout - 16pt.subheadline - 15pt.footnote - 13pt.caption - 12pt.caption2 - 11pt// ❌ 错误 - 固定框架在大文本时损坏
Text("Long product description...")
.font(.body)
.frame(height: 50) // 在大文本大小时被裁剪
// ✅ 正确 - 灵活框架
Text("Long product description...")
.font(.body)
.lineLimit(nil) // 允许多行
.fixedSize(horizontal: false, vertical: true)
// ✅ 正确 - 堆栈在大尺寸时重新排列
HStack {
Text("Label:")
Text("Value")
}
.dynamicTypeSize(...DynamicTypeSize.xxxLarge) // 如果需要,限制最大尺寸
Xcode 预览:环境覆盖
.environment(\.sizeCategory, .accessibilityExtraExtraExtraLarge)
模拟器:设置 → 无障碍 → 显示与文字大小 → 更大字体 → 拖动到最大
设备:设置 → 无障碍 → 显示与文字大小 → 更大字体
检查:文本是否保持可读?布局是否适应?是否有文本被裁剪?
问题 低对比度文本对于视力障碍用户或在明亮阳光下难以阅读。
// ❌ 错误 - 低对比度(1.8:1 - 不符合 WCAG)
Text("Warning")
.foregroundColor(.yellow) // 在白色背景上
// ❌ 错误 - 深色模式下低对比度
Text("Info")
.foregroundColor(.gray) // 在黑色背景上
// ✅ 正确 - 高对比度(7:1+ 通过 AAA)
Text("Warning")
.foregroundColor(.orange) // 或 .red
// ✅ 正确 - 系统颜色适应浅色/深色模式
Text("Info")
.foregroundColor(.primary) // 浅色模式下为黑色,深色模式下为白色
Text("Secondary")
.foregroundColor(.secondary) // 自动高对比度
// ❌ 错误 - 仅用颜色表示状态
Circle()
.fill(isAvailable ? .green : .red)
// ✅ 正确 - 颜色 + 图标/文本
HStack {
Image(systemName: isAvailable ? "checkmark.circle.fill" : "xmark.circle.fill")
Text(isAvailable ? "Available" : "Unavailable")
}
.foregroundColor(isAvailable ? .green : .red)
// ✅ 正确 - 尊重系统偏好
if UIAccessibility.shouldDifferentiateWithoutColor {
// 使用图案、图标或文本,而非仅用颜色
}
问题 小的点击目标对于运动障碍用户来说难以或无法点击。
WCAG 2.5.5 目标尺寸(AAA 级 - 44x44pt 最小值)
Apple HIG 所有可点击元素的最小值为 44x44pt
// ❌ 错误 - 太小(24x24pt)
Button("×") {
dismiss()
}
.frame(width: 24, height: 24)
// ❌ 错误 - 小图标无内边距
Image(systemName: "heart")
.font(.system(size: 16))
.onTapGesture { }
// ✅ 正确 - 最小 44x44pt
Button("×") {
dismiss()
}
.frame(minWidth: 44, minHeight: 44)
// ✅ 正确 - 更大图标或内边距
Image(systemName: "heart")
.font(.system(size: 24))
.frame(minWidth: 44, minHeight: 44)
.contentShape(Rectangle()) // 扩展点击区域
.onTapGesture { }
// ✅ 正确 - 带边缘内嵌的 UIKit 按钮
button.contentEdgeInsets = UIEdgeInsets(top: 12, left: 12, bottom: 12, right: 12)
// 总尺寸:图标大小 + 内嵌 ≥ 44x44pt
// ❌ 错误 - 目标太近(难以准确点击)
HStack(spacing: 4) {
Button("Edit") { }
Button("Delete") { }
}
// ✅ 正确 - 足够间距(最小 8pt,12pt 更好)
HStack(spacing: 12) {
Button("Edit") { }
Button("Delete") { }
}
问题 无法使用触摸/鼠标的用户无法导航应用。
WCAG 2.1.1 键盘(A 级 - 所有功能均可通过键盘使用)
// ❌ 错误 - 自定义手势无键盘替代方案
.onTapGesture {
showDetails()
}
// 无法用键盘触发
// ✅ 正确 - 按钮自动提供键盘支持
Button("Show Details") {
showDetails()
}
.keyboardShortcut("d", modifiers: .command) // 可选快捷键
// ✅ 正确 - 带焦点支持的自定义控件
struct CustomButton: View {
@FocusState private var isFocused: Bool
var body: some View {
Text("Custom")
.focusable()
.focused($isFocused)
.onKeyPress(.return) {
action()
return .handled
}
}
}
// ✅ 正确 - 设置初始焦点
.focusSection() // 分组相关控件
.defaultFocus($focus, .constant(true)) // 设置默认值
// ✅ 正确 - 操作后移动焦点
@FocusState private var focusedField: Field?
Button("Next") {
focusedField = .next
}
问题 动画会导致前庭障碍用户不适、恶心或癫痫发作。
WCAG 2.3.3 交互动画(AAA 级 - 可以禁用动态动画)
// ❌ 错误 - 总是动画(可能导致恶心)
.onAppear {
withAnimation(.spring(response: 0.6, dampingFraction: 0.8)) {
scale = 1.0
}
}
// ❌ 错误 - 视差滚动无退出选项
ScrollView {
GeometryReader { geo in
Image("hero")
.offset(y: geo.frame(in: .global).minY * 0.5) // 视差
}
}
// ✅ 正确 - 尊重减少动态效果偏好
.onAppear {
if UIAccessibility.isReduceMotionEnabled {
scale = 1.0 // 即时
} else {
withAnimation(.spring(response: 0.6, dampingFraction: 0.8)) {
scale = 1.0
}
}
}
// ✅ 正确 - 更简单的动画或交叉淡入淡出
if UIAccessibility.isReduceMotionEnabled {
// 交叉淡入淡出或即时变更
withAnimation(.linear(duration: 0.2)) {
showView = true
}
} else {
// 复杂的弹簧动画
withAnimation(.spring()) {
showView = true
}
}
// ✅ 正确 - 自动支持
.animation(.spring(), value: isExpanded)
.transaction { transaction in
if UIAccessibility.isReduceMotionEnabled {
transaction.animation = nil // 禁用动画
}
}
// ❌ 错误 - 信息性图像无标签
Image("product-photo")
// ✅ 正确 - 信息性图像带标签
Image("product-photo")
.accessibilityLabel("Red sneakers with white laces")
// ✅ 正确 - 隐藏装饰性图像
Image("background-pattern")
.accessibilityHidden(true)
// ❌ 错误 - 自定义按钮无按钮特征
Text("Submit")
.onTapGesture {
submit()
}
// VoiceOver 宣布为“Submit, text”而非“Submit, button”
// ✅ 正确 - 对类似按钮的控件使用 Button
Button("Submit") {
submit()
}
// VoiceOver 宣布为“Submit, button”
// ✅ 正确 - 具有正确特征的自定义控件
Text("Submit")
.accessibilityAddTraits(.isButton)
.onTapGesture {
submit()
}
// ❌ 错误 - 自定义滑块无无障碍支持
struct CustomSlider: View {
@Binding var value: Double
var body: some View {
// 仅拖拽手势,无 VoiceOver 支持
GeometryReader { geo in
// ...
}
.gesture(DragGesture()...)
}
}
// ✅ 正确 - 带无障碍操作的自定义滑块
struct CustomSlider: View {
@Binding var value: Double
var body: some View {
GeometryReader { geo in
// ...
}
.gesture(DragGesture()...)
.accessibilityElement()
.accessibilityLabel("Volume")
.accessibilityValue("\(Int(value))%")
.accessibilityAdjustableAction { direction in
switch direction {
case .increment:
value = min(value + 10, 100)
case .decrement:
value = max(value - 10, 0)
@unknown default:
break
}
}
}
}
// ❌ 错误 - 状态变更无宣布
Button("Toggle") {
isOn.toggle()
}
// ✅ 正确 - 状态变更带宣布
Button("Toggle") {
isOn.toggle()
UIAccessibility.post(
notification: .announcement,
argument: isOn ? "Enabled" : "Disabled"
)
}
// ✅ 正确 - 带 accessibilityValue 的自动状态
Button("Toggle") {
isOn.toggle()
}
.accessibilityValue(isOn ? "Enabled" : "Disabled")
问题 应用在辅助访问模式下不可用或损坏,排除了依赖简化系统体验的认知障碍用户。
辅助访问是一种全系统模式(设置 > 无障碍 > 辅助访问),用大控件、简化导航和减少认知负荷取代标准 iOS UI。未选择加入此模式的应用会在此模式下对用户隐藏。
您的应用未出现在辅助访问设置的“优化应用”下。
<!-- ✅ 修复 - 添加到 Info.plist -->
<key>UISupportsAssistiveAccess</key>
<true/>
这使应用在辅助访问模式下可用并以全屏启动。没有此键,辅助访问模式下的用户根本无法访问您的应用。
您的应用在辅助访问模式下启动,但显示完整标准界面,让需要简化控件的用户不知所措。
// ✅ 修复 - 提供专用的辅助访问场景
@main
struct MyApp: App {
var body: some Scene {
WindowGroup {
ContentView() // 标准 UI
}
AssistiveAccess {
AssistiveAccessContentView() // 简化 UI
}
}
}
AssistiveAccess 场景类型提供了一个单独的入口点。当系统处于辅助访问模式时,它会使用此场景而非标准 WindowGroup。此场景内的原生 SwiftUI 控件会自动采用辅助访问视觉样式(大按钮、突出导航、网格/行布局)。
如果您的应用已经为认知障碍用户(例如,AAC 应用)专门构建,它可能显示在缩小的框架中而非全屏。
<!-- ✅ 修复 - 为已为认知无障碍设计的应用添加到 Info.plist -->
<key>UISupportsFullScreenInAssistiveAccess</key>
<true/>
这使您的应用与其标准外观完全相同地显示,绕过辅助访问框架。
struct MyView: View {
@Environment(\.accessibilityAssistiveAccessEnabled) var assistiveAccessEnabled
var body: some View {
if assistiveAccessEnabled {
// 简化内容
} else {
// 标准内容
}
}
}
对于 UIKit 应用,在您的 UISceneConfiguration 中使用 .windowAssistiveAccessApplication 场景会话角色,以路由到专用于辅助访问体验的场景委托。
NavigationStack {
MyView()
.navigationTitle("My Feature")
.assistiveAccessNavigationIcon(systemImage: "star.fill")
}
Xcode → 打开开发者工具 → 无障碍检查器
.accessibilityAdjustableAction)VoiceOver 支持
动态类型
足够的对比度
提交时:
无障碍 → 选择您的应用支持的功能:
测试备注:记录无障碍测试
Accessibility Testing Completed:
- VoiceOver: All screens tested with VoiceOver enabled
- Dynamic Type: Tested at all size categories
- Color Contrast: Verified 4.5:1 minimum contrast
- Touch Targets: All buttons minimum 44x44pt
- Reduce Motion: Animations respect user preference
“应用在 VoiceOver 下功能不全”
“文本在所有动态类型尺寸下不可读”
“色彩对比度不足”
在设计评审压力下,您将面临以下要求:
这些听起来像是合理的设计偏好。但它们违反了 App Store 要求并排除了 15% 的用户。 您的职责:使用 App Store 指南和法律要求来辩护,而非个人意见。
如果您听到以下任何一项,请停止并参考此技能:
"我想支持这个设计方向,但让我向您展示 Apple 的 App Store 审核指南 2.5.1:
'应用应支持无障碍功能,如 VoiceOver 和动态类型。
未包含足够的无障碍功能可能导致被拒。'
这是我们需要通过的审核要求:
1. 所有交互元素上的 VoiceOver 标签
2. 动态类型支持(不能锁定字体大小)
3. 文本 4.5:1 对比度比率,UI 3:1
4. 最小 44x44pt 触摸目标
让我展示我们当前设计不足的地方..."
开启无障碍功能打开应用:
"我可以在满足无障碍要求的同时实现您的审美目标:
1. VoiceOver 标签:以编程方式添加(在 UI 中不可见,审核必需)
2. 动态类型:使用适应的布局技术(来自 Apple HIG 的示例)
3. 对比度:稍微调整颜色以满足 4.5:1(我会展示保留品牌的选项)
4. 触摸目标:以编程方式扩展命中区域(视觉大小保持不变)
这些更改不会影响您看到的视觉设计,但它们是 App Store 审核和法律合规所必需的。"
如果被否决(设计师坚持违规):
Slack 消息给 PM + 设计师:
"设计评审决定继续进行:
- 固定字体大小(禁用动态类型)
- 38x38pt 按钮(低于 44pt 要求)
- 3.8:1 文本对比度(低于 4.5:1 要求)
重要:这些更改违反了 App Store 审核指南 2.5.1 和 WCAG AA。
这带来了三个风险:
1. 审核期间 App Store 被拒(增加 1-2 周延迟)
2. 如果用户投诉,存在 ADA 合规问题(法律风险)
3. 15% 的潜在用户无法有效使用应用
我主动标记此问题,以便在被拒时我们可以准备应对计划。"
// ❌ 错误 - 通用标签(将无法通过重新审核)
Button(action: addToCart) {
Image(systemName: "cart.badge.plus")
}
.accessibilityLabel("Button") // Apple 将再次拒绝
// ✅ 正确 - 描述性标签(通过审核)
Button(action: addToCart) {
Image(systemName: "cart.badge.plus")
}
.accessibilityLabel("Add to cart")
.accessibilityHint("Double-tap to add this item to your shopping cart")
时间估计 2-4 小时审计所有交互元素并添加适当标签。
有时设计师有正当理由覆盖无障碍指南。如果满足以下条件,请接受:
"设计评审决定继续进行[具体违规]。
我们理解这会造成:
- App Store 被拒风险(指南 2.5.1)
- 如果被拒,潜在 1-2 周延迟
- 如果被拒,需要审计并修复所有实例
监控计划:
- 以当前设计提交审核
- 如果被拒,实施适当的无障碍功能(估计 2-4 小时)
- 准备好符合无障碍要求的版本作为备份"
这保护了双方,并表明您不是在阻碍 - 只是在降低风险。
目标 所有内容达到 AA 级,可行之处达到 AAA 级。
修复后:
# 快速扫描新问题
/axiom:audit-accessibility
# 特定问题的深度诊断
/skill axiom:accessibility-diag
文档 : /accessibility/voiceover, /uikit/uifont/scaling_fonts_automatically
请记住 无障碍功能不是一项特性,而是一项要求。15% 的用户有某种形式的残疾。让您的应用可访问不仅是正确的事情 - 它还扩大了您的用户群并改善了每个人的体验。
每周安装
Systematic accessibility diagnosis and remediation for iOS/macOS apps. Covers the 7 most common accessibility issues that cause App Store rejections and user complaints.
Core principle Accessibility is not optional. iOS apps must support VoiceOver, Dynamic Type, and sufficient color contrast to pass App Store Review. Users with disabilities depend on these features.
Problem Missing or generic accessibility labels prevent VoiceOver users from understanding UI purpose.
WCAG 4.1.2 Name, Role, Value (Level A)
// ❌ WRONG - No label (VoiceOver says "Button")
Button(action: addToCart) {
Image(systemName: "cart.badge.plus")
}
// ❌ WRONG - Generic label
.accessibilityLabel("Button")
// ❌ WRONG - Reads implementation details
.accessibilityLabel("cart.badge.plus") // VoiceOver: "cart dot badge dot plus"
// ✅ CORRECT - Descriptive label
Button(action: addToCart) {
Image(systemName: "cart.badge.plus")
}
.accessibilityLabel("Add to cart")
// ✅ CORRECT - With hint for complex actions
.accessibilityLabel("Add to cart")
.accessibilityHint("Double-tap to add this item to your shopping cart")
// ✅ CORRECT - Hide decorative images from VoiceOver
Image("decorative-pattern")
.accessibilityHidden(true)
// ✅ CORRECT - Combine multiple elements into one label
HStack {
Image(systemName: "star.fill")
Text("4.5")
Text("(234 reviews)")
}
.accessibilityElement(children: .combine)
.accessibilityLabel("Rating: 4.5 stars from 234 reviews")
Problem Fixed font sizes prevent users with vision disabilities from reading text.
WCAG 1.4.4 Resize Text (Level AA - support 200% scaling without loss of content/functionality)
// ❌ WRONG - Fixed size, won't scale
Text("Price: $19.99")
.font(.system(size: 17))
UILabel().font = UIFont.systemFont(ofSize: 17)
// ❌ WRONG - Custom font without scaling
Text("Headline")
.font(Font.custom("CustomFont", size: 24))
// ✅ CORRECT - SwiftUI semantic styles (auto-scales)
Text("Price: $19.99")
.font(.body)
Text("Headline")
.font(.headline)
// ✅ CORRECT - UIKit semantic styles
label.font = UIFont.preferredFont(forTextStyle: .body)
// ✅ CORRECT - Custom font with scaling
let customFont = UIFont(name: "CustomFont", size: 24)!
label.font = UIFontMetrics.default.scaledFont(for: customFont)
label.adjustsFontForContentSizeCategory = true
// ❌ WRONG - Fixed size, won't scale
Text("Price: $19.99")
.font(.system(size: 17))
// ⚠️ ACCEPTABLE - Custom font without scaling (accessibility violation)
Text("Headline")
.font(Font.custom("CustomFont", size: 24))
// ✅ GOOD - Custom size that scales with Dynamic Type
Text("Large Title")
.font(.system(size: 60).relativeTo(.largeTitle))
Text("Custom Headline")
.font(.system(size: 24).relativeTo(.title2))
// ✅ BEST - Use semantic styles when possible
Text("Headline")
.font(.headline)
HowrelativeTo: works
.title2, .largeTitle, etc.)Example
.title2 base: ~22pt → Your custom: 24pt (1.09x larger).title2 grows to ~28pt → Your custom grows to ~30.5pt (maintains 1.09x ratio)Fix hierarchy (best to worst)
.title, .body, .caption).system(size:).relativeTo() for required custom sizes.dynamicTypeSize() modifier.largeTitle - 34pt (scales to 44pt at accessibility sizes).title - 28pt.title2 - 22pt.title3 - 20pt.headline - 17pt semibold.body - 17pt (default).callout - 16pt.subheadline - 15pt.footnote - 13pt.caption - 12pt// ❌ WRONG - Fixed frame breaks with large text
Text("Long product description...")
.font(.body)
.frame(height: 50) // Clips at large text sizes
// ✅ CORRECT - Flexible frame
Text("Long product description...")
.font(.body)
.lineLimit(nil) // Allow multiple lines
.fixedSize(horizontal: false, vertical: true)
// ✅ CORRECT - Stack rearranges at large sizes
HStack {
Text("Label:")
Text("Value")
}
.dynamicTypeSize(...DynamicTypeSize.xxxLarge) // Limit maximum size if needed
Xcode Preview: Environment override
.environment(\.sizeCategory, .accessibilityExtraExtraExtraLarge)
Simulator: Settings → Accessibility → Display & Text Size → Larger Text → Drag to maximum
Device: Settings → Accessibility → Display & Text Size → Larger Text
Check: Does text remain readable? Does layout adapt? Is any text clipped?
Problem Low contrast text is unreadable for users with vision disabilities or in bright sunlight.
// ❌ WRONG - Low contrast (1.8:1 - fails WCAG)
Text("Warning")
.foregroundColor(.yellow) // on white background
// ❌ WRONG - Low contrast in dark mode
Text("Info")
.foregroundColor(.gray) // on black background
// ✅ CORRECT - High contrast (7:1+ passes AAA)
Text("Warning")
.foregroundColor(.orange) // or .red
// ✅ CORRECT - System colors adapt to light/dark mode
Text("Info")
.foregroundColor(.primary) // Black in light mode, white in dark
Text("Secondary")
.foregroundColor(.secondary) // Automatic high contrast
// ❌ WRONG - Color alone indicates status
Circle()
.fill(isAvailable ? .green : .red)
// ✅ CORRECT - Color + icon/text
HStack {
Image(systemName: isAvailable ? "checkmark.circle.fill" : "xmark.circle.fill")
Text(isAvailable ? "Available" : "Unavailable")
}
.foregroundColor(isAvailable ? .green : .red)
// ✅ CORRECT - Respect system preference
if UIAccessibility.shouldDifferentiateWithoutColor {
// Use patterns, icons, or text instead of color alone
}
Problem Small tap targets are difficult or impossible for users with motor disabilities.
WCAG 2.5.5 Target Size (Level AAA - 44x44pt minimum)
Apple HIG 44x44pt minimum for all tappable elements
// ❌ WRONG - Too small (24x24pt)
Button("×") {
dismiss()
}
.frame(width: 24, height: 24)
// ❌ WRONG - Small icon without padding
Image(systemName: "heart")
.font(.system(size: 16))
.onTapGesture { }
// ✅ CORRECT - Minimum 44x44pt
Button("×") {
dismiss()
}
.frame(minWidth: 44, minHeight: 44)
// ✅ CORRECT - Larger icon or padding
Image(systemName: "heart")
.font(.system(size: 24))
.frame(minWidth: 44, minHeight: 44)
.contentShape(Rectangle()) // Expand tap area
.onTapGesture { }
// ✅ CORRECT - UIKit button with edge insets
button.contentEdgeInsets = UIEdgeInsets(top: 12, left: 12, bottom: 12, right: 12)
// Total size: icon size + insets ≥ 44x44pt
// ❌ WRONG - Targets too close (hard to tap accurately)
HStack(spacing: 4) {
Button("Edit") { }
Button("Delete") { }
}
// ✅ CORRECT - Adequate spacing (8pt minimum, 12pt better)
HStack(spacing: 12) {
Button("Edit") { }
Button("Delete") { }
}
Problem Users who cannot use touch/mouse cannot navigate app.
WCAG 2.1.1 Keyboard (Level A - all functionality available via keyboard)
// ❌ WRONG - Custom gesture without keyboard alternative
.onTapGesture {
showDetails()
}
// No way to trigger with keyboard
// ✅ CORRECT - Button provides keyboard support automatically
Button("Show Details") {
showDetails()
}
.keyboardShortcut("d", modifiers: .command) // Optional shortcut
// ✅ CORRECT - Custom control with focus support
struct CustomButton: View {
@FocusState private var isFocused: Bool
var body: some View {
Text("Custom")
.focusable()
.focused($isFocused)
.onKeyPress(.return) {
action()
return .handled
}
}
}
// ✅ CORRECT - Set initial focus
.focusSection() // Group related controls
.defaultFocus($focus, .constant(true)) // Set default
// ✅ CORRECT - Move focus after action
@FocusState private var focusedField: Field?
Button("Next") {
focusedField = .next
}
Problem Animations cause discomfort, nausea, or seizures for users with vestibular disorders.
WCAG 2.3.3 Animation from Interactions (Level AAA - motion animation can be disabled)
// ❌ WRONG - Always animates (can cause nausea)
.onAppear {
withAnimation(.spring(response: 0.6, dampingFraction: 0.8)) {
scale = 1.0
}
}
// ❌ WRONG - Parallax scrolling without opt-out
ScrollView {
GeometryReader { geo in
Image("hero")
.offset(y: geo.frame(in: .global).minY * 0.5) // Parallax
}
}
// ✅ CORRECT - Respect Reduce Motion preference
.onAppear {
if UIAccessibility.isReduceMotionEnabled {
scale = 1.0 // Instant
} else {
withAnimation(.spring(response: 0.6, dampingFraction: 0.8)) {
scale = 1.0
}
}
}
// ✅ CORRECT - Simpler animation or cross-fade
if UIAccessibility.isReduceMotionEnabled {
// Cross-fade or instant change
withAnimation(.linear(duration: 0.2)) {
showView = true
}
} else {
// Complex spring animation
withAnimation(.spring()) {
showView = true
}
}
// ✅ CORRECT - Automatic support
.animation(.spring(), value: isExpanded)
.transaction { transaction in
if UIAccessibility.isReduceMotionEnabled {
transaction.animation = nil // Disable animation
}
}
// ❌ WRONG - Informative image without label
Image("product-photo")
// ✅ CORRECT - Informative image with label
Image("product-photo")
.accessibilityLabel("Red sneakers with white laces")
// ✅ CORRECT - Decorative image hidden
Image("background-pattern")
.accessibilityHidden(true)
// ❌ WRONG - Custom button without button trait
Text("Submit")
.onTapGesture {
submit()
}
// VoiceOver announces as "Submit, text" not "Submit, button"
// ✅ CORRECT - Use Button for button-like controls
Button("Submit") {
submit()
}
// VoiceOver announces as "Submit, button"
// ✅ CORRECT - Custom control with correct trait
Text("Submit")
.accessibilityAddTraits(.isButton)
.onTapGesture {
submit()
}
// ❌ WRONG - Custom slider without accessibility support
struct CustomSlider: View {
@Binding var value: Double
var body: some View {
// Drag gesture only, no VoiceOver support
GeometryReader { geo in
// ...
}
.gesture(DragGesture()...)
}
}
// ✅ CORRECT - Custom slider with accessibility actions
struct CustomSlider: View {
@Binding var value: Double
var body: some View {
GeometryReader { geo in
// ...
}
.gesture(DragGesture()...)
.accessibilityElement()
.accessibilityLabel("Volume")
.accessibilityValue("\(Int(value))%")
.accessibilityAdjustableAction { direction in
switch direction {
case .increment:
value = min(value + 10, 100)
case .decrement:
value = max(value - 10, 0)
@unknown default:
break
}
}
}
}
// ❌ WRONG - State change without announcement
Button("Toggle") {
isOn.toggle()
}
// ✅ CORRECT - State change with announcement
Button("Toggle") {
isOn.toggle()
UIAccessibility.post(
notification: .announcement,
argument: isOn ? "Enabled" : "Disabled"
)
}
// ✅ CORRECT - Automatic state with accessibilityValue
Button("Toggle") {
isOn.toggle()
}
.accessibilityValue(isOn ? "Enabled" : "Disabled")
Problem App is unavailable or broken in Assistive Access mode, excluding users with cognitive disabilities who rely on a simplified system experience.
Assistive Access is a system-wide mode (Settings > Accessibility > Assistive Access) that replaces the standard iOS UI with large controls, simplified navigation, and reduced cognitive load. Apps that don't opt in are hidden from users in this mode.
Your app doesn't appear under "Optimized Apps" in Assistive Access settings.
<!-- ✅ FIX - Add to Info.plist -->
<key>UISupportsAssistiveAccess</key>
<true/>
This makes the app available and launches it full screen in Assistive Access mode. Without this key, users in Assistive Access mode cannot access your app at all.
Your app launches in Assistive Access but shows the full standard interface, overwhelming users who need simplified controls.
// ✅ FIX - Provide a dedicated Assistive Access scene
@main
struct MyApp: App {
var body: some Scene {
WindowGroup {
ContentView() // Standard UI
}
AssistiveAccess {
AssistiveAccessContentView() // Simplified UI
}
}
}
The AssistiveAccess scene type provides a separate entry point. When the system is in Assistive Access mode, it uses this scene instead of the standard WindowGroup. Native SwiftUI controls inside this scene automatically adopt the Assistive Access visual style (large buttons, prominent navigation, grid/row layout).
If your app is already purpose-built for users with cognitive disabilities (e.g., AAC apps), it may appear in a reduced frame rather than full screen.
<!-- ✅ FIX - Add to Info.plist for apps already designed for cognitive accessibility -->
<key>UISupportsFullScreenInAssistiveAccess</key>
<true/>
This displays your app identically to its standard appearance, bypassing the Assistive Access frame.
struct MyView: View {
@Environment(\.accessibilityAssistiveAccessEnabled) var assistiveAccessEnabled
var body: some View {
if assistiveAccessEnabled {
// Simplified content
} else {
// Standard content
}
}
}
For UIKit apps, use the .windowAssistiveAccessApplication scene session role in your UISceneConfiguration to route to a dedicated scene delegate for the Assistive Access experience.
NavigationStack {
MyView()
.navigationTitle("My Feature")
.assistiveAccessNavigationIcon(systemImage: "star.fill")
}
Xcode → Open Developer Tool → Accessibility Inspector
.accessibilityAdjustableAction)VoiceOver Support
Dynamic Type
Sufficient Contrast
When submitting:
Accessibility → Select features your app supports:
Test Notes: Document accessibility testing
Accessibility Testing Completed:
- VoiceOver: All screens tested with VoiceOver enabled
- Dynamic Type: Tested at all size categories
- Color Contrast: Verified 4.5:1 minimum contrast
- Touch Targets: All buttons minimum 44x44pt
- Reduce Motion: Animations respect user preference
"App is not fully functional with VoiceOver"
"Text is not readable at all Dynamic Type sizes"
"Insufficient color contrast"
Under design review pressure, you'll face requests to:
These sound like reasonable design preferences. But they violate App Store requirements and exclude 15% of users. Your job: defend using App Store guidelines and legal requirements, not opinion.
If you hear ANY of these, STOP and reference this skill :
"I want to support this design direction, but let me show you Apple's App Store
Review Guideline 2.5.1:
'Apps should support accessibility features such as VoiceOver and Dynamic Type.
Failure to include sufficient accessibility features may result in rejection.'
Here's what we need for approval:
1. VoiceOver labels on all interactive elements
2. Dynamic Type support (can't lock font sizes)
3. 4.5:1 contrast ratio for text, 3:1 for UI
4. 44x44pt minimum touch targets
Let me show where our design currently falls short..."
Open the app with accessibility features enabled:
"I can achieve your aesthetic goals while meeting accessibility requirements:
1. VoiceOver labels: Add them programmatically (invisible in UI, required for approval)
2. Dynamic Type: Use layout techniques that adapt (examples from Apple HIG)
3. Contrast: Adjust colors slightly to meet 4.5:1 (I'll show options that preserve brand)
4. Touch targets: Expand hit areas programmatically (visual size stays the same)
These changes won't affect the visual design you're seeing, but they're required
for App Store approval and legal compliance."
If overruled (designer insists on violations):
Slack message to PM + designer:
"Design review decided to proceed with:
- Fixed font sizes (disabling Dynamic Type)
- 38x38pt buttons (below 44pt requirement)
- 3.8:1 text contrast (below 4.5:1 requirement)
Important: These changes violate App Store Review Guideline 2.5.1 and WCAG AA.
This creates three risks:
1. App Store rejection during review (adds 1-2 week delay)
2. ADA compliance issues if user files complaint (legal risk)
3. 15% of potential users unable to use app effectively
I'm flagging this proactively so we can prepare a response plan if rejected."
// ❌ WRONG - Generic labels (will fail re-review)
Button(action: addToCart) {
Image(systemName: "cart.badge.plus")
}
.accessibilityLabel("Button") // Apple will reject again
// ✅ CORRECT - Descriptive labels (passes review)
Button(action: addToCart) {
Image(systemName: "cart.badge.plus")
}
.accessibilityLabel("Add to cart")
.accessibilityHint("Double-tap to add this item to your shopping cart")
Time estimate 2-4 hours to audit all interactive elements and add proper labels.
Sometimes designers have valid reasons to override accessibility guidelines. Accept if:
"Design review decided to proceed with [specific violations].
We understand this creates:
- App Store rejection risk (Guideline 2.5.1)
- Potential 1-2 week delay if rejected
- Need to audit and fix all instances if rejected
Monitoring plan:
- Submit for review with current design
- If rejected, implement proper accessibility (estimated 2-4 hours)
- Have accessibility-compliant version ready as backup"
This protects both of you and shows you're not blocking - just de-risking.
Goal Meet Level AA for all content, Level AAA where feasible.
After making fixes:
# Quick scan for new issues
/axiom:audit-accessibility
# Deep diagnosis for specific issues
/skill axiom:accessibility-diag
Docs : /accessibility/voiceover, /uikit/uifont/scaling_fonts_automatically
Remember Accessibility is not a feature, it's a requirement. 15% of users have some form of disability. Making your app accessible isn't just the right thing to do - it expands your user base and improves the experience for everyone.
Weekly Installs
115
Repository
GitHub Stars
617
First Seen
Jan 21, 2026
Security Audits
Gen Agent Trust HubPassSocketPassSnykPass
Installed on
opencode98
claude-code91
codex91
gemini-cli89
cursor87
github-copilot83
Flutter布局指南:构建响应式UI的约束规则与自适应设计模式
1,200 周安装
.caption2 - 11pt