Highlight
Apple 在 Xcode 15 中为 XCTest 框架添加了
performAccessibilityAudit()方法,开发者可以在 UI 测试中一键自动检查 VoiceOver 标签、对比度、动态字体等 10 种无障碍问题,并通过闭包过滤误报,将无障碍测试集成到 CI/CD 流程中。
核心内容
问题:为什么无障碍测试总被推迟
很多开发者想做无障碍适配,但现实是:项目一紧,无障碍优化总是最先被砍掉。原因很简单——测试成本太高。
传统的无障碍测试流程是:手动开启 VoiceOver,在屏幕上一个个滑动、点击,听标签是否正确描述界面元素。这个过程有几个问题:
- 耗时长:完整测试一个中等复杂的界面需要 10-20 分钟
- 难以回归:每次 UI 改动后都需要重新手动测试
- 容易遗漏:人眼和耳朵会疲劳,对比度问题、字体缩放后的布局问题很容易漏掉
- 无法自动化:没有工具能在 CI 中自动跑无障碍检查
结果是:大部分 App 只在发版前做一次无障碍测试,或者干脆不做。
Apple 的方案:自动化无障碍审计
Xcode 15 引入了 XCUIApplication.performAccessibilityAudit() 方法,可以在 UI 测试中自动检测 10 种无障碍问题:
- 动态字体:元素在字体放大后是否会被裁剪
- 对比度:文字和背景的对比度是否达到 WCAG AA 标准
- 元素存在性:可交互元素是否对辅助技术可见
- 标签:按钮、图片是否有描述性的
accessibilityLabel - 特征:交互元素是否声明了正确的特征(如按钮是否是
.button) - 提示:是否为状态变化提供了
accessibilityHint - 可访问性:禁用状态是否正确反映
- 动作:自定义交互是否提供了
accessibilityCustomActions - 命中测试:点击区域是否足够大(至少 44x44 点)
- 键盘焦点:键盘导航顺序是否合理
这些检测在 UI 测试中运行,和普通的 XCTAssert 一样——失败就 fail 测试,成功就继续。这意味着你可以把无障碍测试放入持续集成流程,每次构建都自动跑一遍。
详细内容
基础用法:一键审计整个 App
最简单的用法只需要三行代码(02:52):
func testAccessibility() throws {
let app = XCUIApplication()
app.launch()
try app.performAccessibilityAudit()
}
关键点:
XCUIApplication()是 UI 测试中的 App 进程代理,launch()启动 AppperformAccessibilityAudit()会遍历当前可见的所有界面元素,依次检查 10 种无障碍问题- 发现任何问题就抛出异常,测试失败
- 这个方法会检查当前屏幕上可见的所有元素,包括那些
isHittable == false的元素
过滤误报:自定义审计规则
实际使用中,有些”问题”是设计决策,不是真正的 bug。比如一个纯装饰性的背景图没有标签、一个低对比度的文字是品牌色要求。
Apple 提供了闭包参数,让你在每个问题发现时决定是否忽略(09:57):
try app.performAccessibilityAudit(for: [.dynamicType, .contrast]) { issue in
var shouldIgnore = false
// 忽略 "My Label" 的对比度问题
if let element = issue.element,
element.label == "My Label",
issue.auditType == .contrast {
shouldIgnore = true
}
return shouldIgnore
}
关键点:
for:参数指定只审计哪些类型,传空数组[]表示审计全部 10 种类型issue是AccessibilityIssue类型,包含element(出问题的 UI 元素)、auditType(问题类型)、message(具体描述)- 闭包返回
true表示忽略这个问题,false表示让它 fail 测试 - 可以根据元素的
label、identifier、elementType等属性判断是否是误报 - 这个闭包只影响当前这次审计调用,不会修改全局配置
控制审计范围:只暴露特定元素给辅助技术
有些界面元素你希望 VoiceOver 能访问,但不希望被 UI 测试访问(比如一些内部的布局容器),或者反过来希望 UI 测试能访问但 VoiceOver 不需要。
iOS 17 新增了 accessibilityElements 属性来精确控制 VoiceOver 可见的元素范围(08:40):
// 只让 VoiceOver 看到这两个元素
view.accessibilityElements = [quoteTextView, newQuoteButton]
关键点:
accessibilityElements是UIView和NSView的属性- 设置后,辅助技术只能看到这个数组里的元素,子视图会被忽略
- 这是一种”白名单”模式,适合不想暴露内部布局结构的场景
对应地,UI 测试的可见性也可以单独控制(14:07):
// 让 UI 测试只能访问这三个元素
view.automationElements = [imageView, quoteTextView, newQuoteButton]
关键点:
automationElements控制 XCTest 能看到的元素- 这对于不想让测试代码耦合到实现细节的场景很有用
- 设置后,
XCUIApplication的查询只能找到数组里的元素
审计结果解读:如何修复失败
当 performAccessibilityAudit() 发现问题时,测试日志会输出类似这样的信息:
Accessibility Issue - Contrast
Element: Button, label = "Submit"
Issue: Contrast ratio is 2.1:1, which is below the WCAG AA standard of 4.5:1 for normal text
常见的修复方式:
| 问题类型 | 典型原因 | 修复方案 |
|---|---|---|
| 对比度 | 文字颜色和背景太接近 | 提高文字颜色亮度或降低背景颜色亮度,或加粗文字(小字号时粗体要求降到 3:1) |
| 标签 | 按钮/图片没设 accessibilityLabel | 为装饰性元素设 accessibilityLabel = "",为功能元素提供描述性标签 |
| 动态字体 | 固定高度的约束导致字体放大后被裁剪 | 使用 contentHuggingPriority 和 compressionResistancePriority,或用 UIStackView 让子视图自适应 |
| 动作 | 自定义手势没提供替代操作 | 添加 accessibilityCustomActions 为 VoiceOver 用户提供菜单式操作 |
核心启发
1. 为每个 UI 测试加上无障碍审计
从今天起,每个主要界面的 UI 测试都加上 try app.performAccessibilityAudit()。这就像为测试套件买了一份”无障碍保险”——以后任何人修改这个界面,只要破坏了无障碍属性,测试就会立刻失败。
实现步骤:
- 找到项目中覆盖率最高的 UI 测试(通常是登录流程、主页导航等)
- 在
app.launch()后加一行try app.performAccessibilityAudit() - 跑一次测试,记录首次发现的问题数量
- 逐个修复,或者先用过滤闭包屏蔽已知问题,后续慢慢清理
2. 创建”无障碍检查清单”模板
很多无障碍问题是模式化的:导航栏返回按钮缺标签、卡片视图的点击区域太小、图表没有 accessibilityValue。创建一个项目级 checklist,每次新界面开发完对照检查。
模板示例:
- 所有按钮都有
accessibilityLabel或有意义的title - 图片如果是装饰性的,设
accessibilityElementsHidden = true - 可点击元素最小尺寸 44x44 点
- 文字和背景对比度 >= 4.5:1(正常字号)或 3:1(大字号/粗体)
- 自定义手势提供
accessibilityCustomActions或替代操作
3. 为品牌色系统建立无障碍对比度测试
很多 App 的品牌色系统最初设计时没考虑对比度,导致整个 App 的文字阅读困难。写一个脚本,自动计算你的品牌色盘和所有文字颜色的对比度,生成一份”无障碍友好的配色建议”。
实现思路:
func calculateContrastRatio(foreground: UIColor, background: UIColor) -> CGFloat {
// 计算 WCAG 对比度
// 公式:(L1 + 0.05) / (L2 + 0.05),其中 L1 是较亮颜色的相对亮度
// 返回值 >= 4.5 表示达到 AA 标准
}
然后遍历你的 Design Token 里的所有 foregroundColor 和 backgroundColor 组合,输出不满足 4.5:1 的组合清单。
4. 用 automationElements 减少 UI 测试的脆弱性
UI 测试经常因为界面调整(比如加了一个中间容器视图、改了视图层级)而失败。用 automationElements 只暴露真正重要的测试锚点,让测试代码不耦合到实现细节。
适用场景:
- 复杂的自定义控件(如日历、图表),内部子视图经常变动
- 使用第三方组件库时,不想让测试依赖库内部结构
- 需要在 UI 测试中隐藏一些调试用的辅助视图
5. 建立无障碍问题的分级处理流程
不是所有无障碍问题都需要立即修复。根据严重程度建立分级:
- P0:阻塞基本使用(如主要按钮没标签、对比度太低导致完全看不见)—— 必须在本次发版前修复
- P1:影响特定用户群体(如自定义手势没替代操作)—— 下个版本修复
- P2:优化体验(如 Hint 缺失、动作描述不够详细)—— 逐步改善
- P3:误报或设计决策(如品牌色对比度略低但有其他补偿)—— 记录原因,用审计闭包过滤
这样可以平衡发版速度和无障碍质量。
关联 Session
- What’s new in accessibility — 了解 iOS 17、iPadOS 17、macOS Sonoma 中的最新无障碍特性和框架
- Testing tips and tricks — 掌握编写更好测试的技巧,包括 UI 测试和单元测试策略
- Meet Apple Store Accessibility: A new way to shop — 探索 Apple Store 的无障碍功能如何增强残障用户的购物体验
- Design for accessibility — 学习如何设计适合所有人的界面,包括视觉、运动和认知障碍用户
- Accessibility in SwiftUI — 发现 SwiftUI 如何让构建无障碍界面变得简单,以及如何定制无障碍体验
评论
GitHub Issues · utterances