Accessibility 高级 22m
为数据密集型 App 定制 VoiceOver 体验
Tailor the VoiceOver experience in your data-rich apps
2021年6月11日
一句话判断
如果你的 app 有表格、图表、列表等数据密集界面,VoiceOver 默认的朗读方式会让视障用户完全迷失 —— 这集 Session 教你如何用自定义 rotor、announcement 通知和智能分组来重建数据的信息层次。
这场 Session 讲了什么
Session 聚焦于数据密集型 app(如股票追踪、健身记录、任务管理)的 VoiceOver 优化策略。当界面中有大量数字、状态、图表时,VoiceOver 默认的”逐个朗读所有元素”方式效率极低,甚至让用户无法理解数据含义。
核心策略:
- 自定义 Rotor:添加自定义的筛选维度(如”按状态筛选”、“按优先级跳转”),让 VoiceOver 用户可以快速在特定类型的数据间跳转。
- 动态 Announcement:在数据更新时主动通知 VoiceOver 用户,而不是等他们手动刷新。
- Smart Grouping(智能分组):把相关的数据点组织成有意义的单元,减少 VoiceOver 的停留次数。
- Custom Actions:为每个数据项提供上下文相关的操作列表(如”标记完成”、“删除”、“编辑”),通过上下滑动选择。
值得深挖的点
自定义 Rotor 的实现。Rotor 是 VoiceOver 的一个导航维度,默认只有”字符”、“词”、“标题”等。你可以添加自定义 rotor 选项(如”按项目状态跳转”),让用户在转子菜单中选择后,上下滑动只在匹配的项目间跳转。实现方式是创建 AccessibilityCustomContent 和 UIAccessibilityCustomRotor,过滤和排序由你决定。
数据更新的无障碍通知。当数据实时变化时(如股票价格波动、新消息到达),你需要主动通知 VoiceOver 用户。使用 UIAccessibility.post(notification: .announcement, argument: "苹果股价上涨 2%") 发送语音通知。但要控制频率 —— 如果股价每秒变化都播报,用户会被淹没。建议只在关键变化(涨跌幅超过阈值)时发送。
代码片段
SwiftUI 自定义 Rotor:
import SwiftUI
struct TaskListView: View {
let tasks: [Task]
@AccessibilityFocusState private var focusedTaskId: UUID?
var body: some View {
List(tasks) { task in
TaskRowView(task: task)
.accessibilityElement(children: .combine)
.accessibilityLabel(taskDescription(task))
.accessibilityAddTraits(.isButton)
.accessibilityFocused($focusedTaskId, equals: task.id)
// 自定义操作
.accessibilityAction(named: "标记完成") {
markComplete(task)
}
.accessibilityAction(named: "删除") {
deleteTask(task)
}
}
// 自定义 Rotor:按优先级跳转
.accessibilityRotor("高优先级任务") {
ForEach(tasks.filter { $0.priority == .high }) { task in
AccessibilityRotorEntry(task.title, id: task.id) {
focusedTaskId = task.id
}
}
}
.accessibilityRotor("今日截止") {
ForEach(tasks.filter { Calendar.current.isDateInToday($0.dueDate) }) { task in
AccessibilityRotorEntry(task.title, id: task.id) {
focusedTaskId = task.id
}
}
}
}
private func taskDescription(_ task: Task) -> String {
let priorityText = task.priority == .high ? "高优先级" : "普通"
let statusText = task.isComplete ? "已完成" : "未完成"
return "\(task.title), \(priorityText), \(statusText), 截止 \(task.dueDate.formatted(date: .abbreviated, time: .shortened))"
}
}
数据更新的无障碍通知:
import SwiftUI
class StockViewModel: ObservableObject {
@Published var stockPrice: Double = 0
private var lastAnnouncedPrice: Double = 0
func updatePrice(_ newPrice: Double) {
let oldPrice = stockPrice
stockPrice = newPrice
// 只在价格变化超过 1% 时通知 VoiceOver
let changePercent = abs(newPrice - oldPrice) / oldPrice * 100
if changePercent > 1.0 {
let direction = newPrice > oldPrice ? "上涨" : "下跌"
let message = "苹果股价\(direction)至 \(String(format: "%.2f", newPrice)) 美元"
// 发送 VoiceOver 通知
UIAccessibility.post(notification: .announcement, argument: message)
lastAnnouncedPrice = newPrice
}
}
}
数据表格的无障碍分组:
import SwiftUI
struct DataTableRow: View {
let entry: DataEntry
var body: some View {
HStack {
Text(entry.name).frame(width: 120)
Text("\(entry.value, specifier: "%.1f")").frame(width: 80)
Text(entry.status.rawValue).frame(width: 80)
Text(entry.date.formatted(date: .abbreviated, time: .omitted))
}
// 把一行数据组合为一个 VoiceOver 元素
.accessibilityElement(children: .combine)
.accessibilityLabel {
// 按重要性排序描述
"\(entry.name), \(entry.status.rawValue), 数值 \(entry.value, specifier: "%.1f"), \(entry.date.formatted(date: .abbreviated, time: .omitted))"
}
}
}
最佳实践
- 数据朗读要有人类友好的措辞。不要读”1,523.50”,要读”一千五百二十三点五零美元”。
NumberFormatter的.spellOut风格在 VoiceOver 场景中更合适。 - Rotor 维度不要超过 5 个。太多选项让 VoiceOver 用户在转子菜单中迷失。选择最常用的筛选维度即可。
- 实时数据通知的频率要有策略。不是所有数据变化都值得播报。用阈值过滤噪音,只在用户真正关心的变化时通知。
还有什么值得关注
AccessibilityCustomContent可以添加”详细信息”层,用户通过 Rotor 切换到”详细信息”时才听到额外数据。AXCustomContentDataProvider协议支持动态生成无障碍内容。- Xcode 13 的 Accessibility Inspector 可以模拟 Rotor 操作,在开发阶段验证自定义导航是否正确。
WWDC 2021