Build accessible apps with SwiftUI and UIKit
Swift & UI 进阶 20m

用 SwiftUI 和 UIKit 构建无障碍应用

Build accessible apps with SwiftUI and UIKit

2023年6月5日

在 Apple 官方观看视频

一句话判断

如果你的 App 还没认真做过无障碍适配,这场 Session 提供的 API 足够让你低成本地迈出一大步——特别是 isToggle trait 和 AccessibilityNotification 这两个新增能力。

这场 Session 讲了什么

这场 Session 围绕 SwiftUI 和 UIKit 的无障碍新特性展开,聚焦三个方向:新的交互 API、视觉无障碍增强、以及 UIKit 属性同步的改进。

开场用一个照片编辑 App 作为贯穿全场的示例场景。核心议题包括:

  • isToggle trait:自定义按钮现在可以声明为 toggle,VoiceOver 会自动给出”switch button”的描述和”双击切换设置”的提示,无需手动添加 accessibility hint。
  • AccessibilityNotification:全新的 Swift 原生通知 API,统一了 SwiftUI、UIKit、AppKit 三个平台的公告(announcement)、布局变化、屏幕切换、页面滚动通知。最大的亮点是支持公告优先级——high、default、low 三级,high 可以打断当前语音且自身不可被打断,low 会排队等待。这解决了多个公告同时触发时互相干扰的老问题。
  • Accessibility Zoom Action:为图片等可缩放视图添加无障碍缩放支持,VoiceOver 用户可以通过手势完成缩放操作,不再依赖物理触摸捏合。
  • Direct Touch(allowsDirectInteraction):允许指定屏幕区域让 VoiceOver 手势直接穿透到 App,同时可选静音模式。典型场景是钢琴键盘——用户需要快速连续按键,VoiceOver 的朗读反而成了障碍。
  • SwiftUI 视觉无障碍:包括 accessibilityLabeledPair(将标签与控件配对)、accessibilityElement(children:) 的增强、以及新的文本样式适配动态字体。
  • UIKit 属性自动同步:新的 accessibility properties 使用 property observer 模式,当 UI 状态变化时自动更新无障碍属性,不再需要手动在各处同步。

值得深挖的点

公告优先级机制是这次最值得关注的改进。之前多个 announcement 同时触发时,VoiceOver 的朗读顺序基本不可控,这在导航类 App 中是个常见痛点。现在通过 attributed string 设置优先级,可以精确控制用户听到什么、忽略什么。这背后的设计思路是:不是所有信息都同等重要。

Direct Touch 的静音模式也值得仔细研究。它让 VoiceOver 在特定区域只做触摸转发不做朗读,这对乐器类 App、绘图类 App、游戏来说几乎是必需的功能。之前这类 App 的 VoiceOver 体验普遍很差,现在有了官方解决方案。

UIKit 属性自动同步虽然不是性感的新功能,但对维护大型 UIKit 项目来说是实打实的工程改进。以前每个状态变化点都要手动更新 accessibility label/value/traits,遗漏几乎是必然的。

代码片段

SwiftUI 中添加 isToggle trait:

struct FilterButton: View {
    @State private var filterOn = false

    var body: some View {
        Button {
            filterOn.toggle()
        } label: {
            // 按钮外观
        }
        // 添加 isToggle trait,VoiceOver 自动识别为开关按钮
        .accessibilityAddTraits(.isToggle)
    }
}

带优先级的无障碍公告:

// 创建带优先级的 attributed string
var lowPriorityString: AttributedString {
    var str = AttributedString("正在加载相机")
    str.accessibilitySpeechAnnouncementPriority = .low
    return str
}

var highPriorityString: AttributedString {
    var str = AttributedString("相机已就绪")
    str.accessibilitySpeechAnnouncementPriority = .high
    return str
}

// 按优先级发送公告
AccessibilityNotification.Announcement("正在打开相机").post()
AccessibilityNotification.Announcement(lowPriorityString).post()
AccessibilityNotification.Announcement(highPriorityString).post()
// 结果:低优先级不会打断默认公告,高优先级会打断所有

SwiftUI 无障碍缩放操作:

struct ZoomingImageView: View {
    @State private var scale: CGFloat = 1.0

    var body: some View {
        Image("photo")
            .scaleEffect(scale)
            // 添加无障碍缩放操作
            .accessibilityZoomAction { direction in
                switch direction {
                case .zoomIn:
                    scale += 1.0
                case .zoomOut:
                    scale = max(1.0, scale - 1.0)
                }
                AccessibilityNotification.Announcement("\(Int(scale))x 缩放").post()
            }
    }
}

最佳实践

  • 给自定义开关控件加上 isToggle trait,别再手动拼 accessibility hint 了。系统提供的描述比你自己写的更一致。
  • 公告优先级不要滥用。high 优先级意味着”用户必须听到这条信息”,大部分场景用 default 就够了。把 high 留给安全相关或状态关键变更的提示。
  • Direct Touch 区域要做明确的无障碍标注。虽然该区域静音了,但用户第一次探索时需要知道这里可以做什么。
  • UIKit 项目中,把旧的 accessibility 属性迁移到新的 property observer 模式。这是一个低风险高回报的重构——减少手动同步出错的可能。
  • 无障碍缩放操作记得返回缩放倍数的语音反馈,否则用户不知道当前缩放到什么程度了。

还有什么值得关注

  • AccessibilityNotification 是跨平台 API,AppKit 也适用,macOS App 可以同步受益。
  • accessibilityLabeledPair 把标签和控件的关系显式声明,VoiceOver 可以在用户聚焦控件时自动朗读关联标签,对复杂表单场景很有帮助。
  • 这场 Session 没有深入讨论 Switch Control 和 Voice Control 的适配,如果你的 App 需要全面无障碍支持,还需要结合其他 Session 的内容。
  • SF Symbols 5 的动画效果(见 Session 10258)也可以用来增强无障碍反馈——比如用 bounce 动画指示按钮被按下。
WWDC 2023