WWDC Quick Look 💓 By SwiftGGTeam
Perform accessibility audits for your app

Perform accessibility audits for your app

观看原视频

Highlight

Apple 在 Xcode 15 中为 XCTest 框架添加了 performAccessibilityAudit() 方法,开发者可以在 UI 测试中一键自动检查 VoiceOver 标签、对比度、动态字体等 10 种无障碍问题,并通过闭包过滤误报,将无障碍测试集成到 CI/CD 流程中。


核心内容

问题:为什么无障碍测试总被推迟

很多开发者想做无障碍适配,但现实是:项目一紧,无障碍优化总是最先被砍掉。原因很简单——测试成本太高。

传统的无障碍测试流程是:手动开启 VoiceOver,在屏幕上一个个滑动、点击,听标签是否正确描述界面元素。这个过程有几个问题:

  1. 耗时长:完整测试一个中等复杂的界面需要 10-20 分钟
  2. 难以回归:每次 UI 改动后都需要重新手动测试
  3. 容易遗漏:人眼和耳朵会疲劳,对比度问题、字体缩放后的布局问题很容易漏掉
  4. 无法自动化:没有工具能在 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() 启动 App
  • performAccessibilityAudit() 会遍历当前可见的所有界面元素,依次检查 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 种类型
  • issueAccessibilityIssue 类型,包含 element(出问题的 UI 元素)、auditType(问题类型)、message(具体描述)
  • 闭包返回 true 表示忽略这个问题,false 表示让它 fail 测试
  • 可以根据元素的 labelidentifierelementType 等属性判断是否是误报
  • 这个闭包只影响当前这次审计调用,不会修改全局配置

控制审计范围:只暴露特定元素给辅助技术

有些界面元素你希望 VoiceOver 能访问,但不希望被 UI 测试访问(比如一些内部的布局容器),或者反过来希望 UI 测试能访问但 VoiceOver 不需要。

iOS 17 新增了 accessibilityElements 属性来精确控制 VoiceOver 可见的元素范围(08:40):

// 只让 VoiceOver 看到这两个元素
view.accessibilityElements = [quoteTextView, newQuoteButton]

关键点:

  • accessibilityElementsUIViewNSView 的属性
  • 设置后,辅助技术只能看到这个数组里的元素,子视图会被忽略
  • 这是一种”白名单”模式,适合不想暴露内部布局结构的场景

对应地,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 = "",为功能元素提供描述性标签
动态字体固定高度的约束导致字体放大后被裁剪使用 contentHuggingPrioritycompressionResistancePriority,或用 UIStackView 让子视图自适应
动作自定义手势没提供替代操作添加 accessibilityCustomActions 为 VoiceOver 用户提供菜单式操作

核心启发

1. 为每个 UI 测试加上无障碍审计

从今天起,每个主要界面的 UI 测试都加上 try app.performAccessibilityAudit()。这就像为测试套件买了一份”无障碍保险”——以后任何人修改这个界面,只要破坏了无障碍属性,测试就会立刻失败。

实现步骤:

  1. 找到项目中覆盖率最高的 UI 测试(通常是登录流程、主页导航等)
  2. app.launch() 后加一行 try app.performAccessibilityAudit()
  3. 跑一次测试,记录首次发现的问题数量
  4. 逐个修复,或者先用过滤闭包屏蔽已知问题,后续慢慢清理

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 里的所有 foregroundColorbackgroundColor 组合,输出不满足 4.5:1 的组合清单。

4. 用 automationElements 减少 UI 测试的脆弱性

UI 测试经常因为界面调整(比如加了一个中间容器视图、改了视图层级)而失败。用 automationElements 只暴露真正重要的测试锚点,让测试代码不耦合到实现细节。

适用场景:

  • 复杂的自定义控件(如日历、图表),内部子视图经常变动
  • 使用第三方组件库时,不想让测试依赖库内部结构
  • 需要在 UI 测试中隐藏一些调试用的辅助视图

5. 建立无障碍问题的分级处理流程

不是所有无障碍问题都需要立即修复。根据严重程度建立分级:

  • P0:阻塞基本使用(如主要按钮没标签、对比度太低导致完全看不见)—— 必须在本次发版前修复
  • P1:影响特定用户群体(如自定义手势没替代操作)—— 下个版本修复
  • P2:优化体验(如 Hint 缺失、动作描述不够详细)—— 逐步改善
  • P3:误报或设计决策(如品牌色对比度略低但有其他补偿)—— 记录原因,用审计闭包过滤

这样可以平衡发版速度和无障碍质量。


关联 Session

评论

GitHub Issues · utterances