Highlight
Apple 在 VoiceOver 中加入 Audio Graphs,并通过
AXChart、accessibilityChartDescriptor和图表描述对象让开发者把视觉图表转换成可播放、可探索的无障碍数据体验。
核心内容
很多应用都用图表展示数据。健康趋势、财务变化、汽车油耗、出生率曲线,用户一眼就能看到峰值、趋势和异常点。
问题在于,图表默认依赖视觉。盲人和低视力用户看不到线条的走势,也很难从一长串原始数据点中快速判断数据的含义。
过去的补救方式通常是把数据点塞进无障碍树。VoiceOver 可以逐个朗读点位,但用户要自己在脑中拼出走势。数据点一多,导航也会变慢。
Apple 在 WWDC21 给出了一套更直接的方案:先把图表本身做成 VoiceOver 可导航的语义容器,再用 Audio Graphs(音频图表)把数据序列转换成声音。数值越高,音高越高;数值越低,音高越低。用户可以播放整条曲线,也可以拖动探索某个位置的具体值。
这套能力解决同一个痛点:图表的价值不止是单个数字,还包括趋势、形状和异常点。Audio Graphs 让 VoiceOver 用户也能在几秒内得到这些信息。
视觉图表也要先可读
图表无障碍不只服务 VoiceOver。低视力用户、色觉障碍用户,也需要更清楚的视觉编码。
Apple 建议开发者先处理三件事:提高前景和背景的对比度,避免红绿或蓝黄这种容易造成混淆的配色,用符号辅助区分数据系列。比如一条线用圆点,另一条线用方块,用户不依赖颜色也能分辨类别。
如果产品设计限制了默认样式,还可以响应系统无障碍设置。开启 Differentiate Without Color 时增加符号;开启 Increase Contrast 时使用更高对比度的颜色;开启 Reduce Transparency 时减少透明效果。
VoiceOver 需要能进入图表
一个图表视图通常只是自绘内容。VoiceOver 不知道里面有多少数据点,也不知道每个点在屏幕上的位置。
这场 session 的第一步,是把图表视图声明成语义容器,并给每个数据点创建 UIAccessibilityElement。这样,用户可以按 VoiceOver 的导航方式逐个浏览数据点,听到每个点的 X 值和 Y 值。
Audio Graphs 负责表达趋势
逐点导航能回答“某个点是多少”。Audio Graphs 负责回答“这条曲线长什么样”。
开发者实现 AXChart 协议,返回一个 AXChartDescriptor。这个描述对象包含标题、摘要、坐标轴、数据系列。VoiceOver 拿到这些结构化信息后,就能提供播放、交互拖动、数据值朗读和图表详情视图。
详细内容
1. 用视觉规则降低阅读门槛
(07:02)Apple 先从视觉设计讲起。图表应该尽量使用高对比度颜色。演讲中建议用 Xcode 自带的 Accessibility Inspector(无障碍检查器)里的 Color Contrast Calculator(颜色对比度计算器)检查比例,并以至少 4.5:1 为目标。
这部分没有专门的 Code tab 片段。可以把 session 提到的系统设置整理成检查入口。
import UIKit
import SwiftUI
let reduceMotionEnabled = UIAccessibility.isReduceMotionEnabled
struct ChartStyleProbe: View {
@Environment(\.accessibilityReduceMotion) private var reduceMotion
var body: some View {
Text(reduceMotion ? "Reduce Motion" : "Default Motion")
}
}
关键点:
import UIKit引入UIAccessibility。UIAccessibility.isReduceMotionEnabled对应 UIKit 中的 Reduce Motion 设置。import SwiftUI引入 SwiftUI 的环境值系统。@Environment(\.accessibilityReduceMotion)读取 SwiftUI 中的 Reduce Motion 状态。body根据系统设置切换显示内容,真实图表中可以用同样入口减少动画或运动效果。
(09:40)如果默认图表不能一直显示符号,Differentiate Without Color 开启时应补上符号。Increase Contrast 开启时应采用更高对比度颜色。Reduce Transparency 开启时应减少透明效果。
这条原则适合放在图表样式层。线条颜色、点形状、透明度都不要写死在绘制函数里。先集中生成样式,再交给绘制代码使用。
2. 让图表成为 VoiceOver 语义容器
(10:47)演讲中的示例从一个普通 ChartView 开始。视图持有 ChartModel,模型里有标题和数据点。
class ChartView: UIView {
let model: ChartModel
func drawChart() {
// ...
}
}
struct ChartModel {
let title: String
let dataPoints: [DataPoint]
struct DataPoint {
let name: String
let x: Double
let y: Double
}
}
关键点:
ChartView是自定义图表视图。model保存图表需要展示的数据。drawChart()代表已有绘制逻辑,演讲没有展开绘制细节。ChartModel.title可以用作图表的无障碍标签。dataPoints是图表中的点位集合。DataPoint.x和DataPoint.y分别对应 X 轴和 Y 轴数值。
(10:48)下一步是覆盖图表视图的无障碍属性。accessibilityContainerType 用来告诉 VoiceOver:这个视图是一组相关元素。accessibilityLabel 用来朗读图表标题。accessibilityElements 返回图表内部可以导航的元素。
extension ChartView {
public override var accessibilityContainerType: UIAccessibilityContainerType { … }
public override var accessibilityLabel: String? { … }
public override var accessibilityElements: [Any]? {
get {
return model.dataPoints.map { point in
let axElement = UIAccessibilityElement(accessibilityContainer: self)
axElement.accessibilityValue = "\(point.x) cups, \(point.y) lines of code"
axElement.accessibilityFrameInContainerSpace = frameRect(for: point)
return axElement
}
}
set {}
}
private func frameRect(for dataPoint: DataPoint) -> CGRect {
关键点:
accessibilityContainerType应返回语义分组类型,演讲中对应semanticGroup。accessibilityLabel通常使用图表标题,让 VoiceOver 能识别当前图表。accessibilityElements把每个数据点转换成一个可访问元素。UIAccessibilityElement(accessibilityContainer: self)把元素挂到当前图表视图下面。accessibilityValue是 VoiceOver 读出的数据点内容,实际项目中应本地化并处理复数规则。accessibilityFrameInContainerSpace告诉 VoiceOver 这个数据点在图表容器中的位置。frameRect(for:)可以复用绘制图表时已有的点位计算逻辑。
(13:30)如果图表有几百个或几千个点,不要给每个点都创建元素。Apple 建议按合理区间拆分图表,为每个区间创建一个无障碍元素。这样可以改善导航体验,也可以减少性能压力。
3. 为 Audio Graphs 准备结构化模型
(14:23)Audio Graphs 需要的不只是点位列表。VoiceOver 还需要标题、摘要、坐标轴范围和坐标轴标题。
struct ChartModel {
let title: String
let summary: String
let xAxis: Axis
let yAxis: Axis
let data: [DataPoint]
struct Axis {
let title: String
let range: ClosedRange<Double>
}
struct DataPoint {
let name: String
let x: Double
let y: Double
}
}
关键点:
title是图表标题,会进入图表描述对象。summary类似图表的替代文本,用一两句话说明数据最重要的结论。xAxis描述 X 轴。yAxis描述 Y 轴。Axis.title是坐标轴名称。Axis.range是坐标轴可显示的最小值到最大值。data是图表真实数据。DataPoint继续保存名称、X 值和 Y 值。
(17:45)summary 很关键。演讲把它类比为图表的 alt text(替代文本)。它会出现在 Audio Graph Explorer 视图里,帮助 VoiceOver 用户先抓住数据结论,再进入声音探索。
4. 实现 AXChart 协议
(15:08)要启用 Audio Graphs,先导入 Accessibility 框架,再让图表视图遵守 AXChart 协议。这个协议只要求实现一个属性:accessibilityChartDescriptor。
import Accessibility
extension ChartView: AXChart {
public var accessibilityChartDescriptor: AXChartDescriptor? {
get {
}
set {}
}
}
关键点:
import Accessibility引入 Audio Graphs 相关类型。extension ChartView: AXChart表示这个视图可以向系统提供图表描述。accessibilityChartDescriptor是 VoiceOver 获取图表结构的入口。get中需要构造并返回描述对象。set {}保持属性可写接口,示例中没有处理写入。
实现这个属性后,VoiceOver 的 Rotor 中会出现 Audio Graph 入口。用户可以进入 Chart Details,播放音频图表,也可以进入交互模式拖动探索。
5. 描述坐标轴的标题、范围和值朗读方式
(15:35)坐标轴描述对象告诉 VoiceOver:这个轴是数值轴还是分类轴,显示范围是什么,网格线在哪里,数值应该怎么读。
public var accessibilityChartDescriptor: AXChartDescriptor? {
get {
let xAxis = AXNumericDataAxisDescriptor( … )
let yAxis = AXNumericDataAxisDescriptor(title: model.yAxis.title,
range: model.yAxis.range,
gridlinePositions:[],
valueDescriptionProvider: { value in
return "\(value) lines of code"
})
}
set {}
}
关键点:
AXNumericDataAxisDescriptor用于数值坐标轴。xAxis示例中省略了参数,演讲说明如果 X 轴是分类数据,应使用分类坐标轴描述对象。yAxis使用model.yAxis.title作为坐标轴标题。range: model.yAxis.range提供 Y 轴显示范围。gridlinePositions: []表示这个示例没有提供网格线位置。valueDescriptionProvider控制 VoiceOver 如何朗读数值。"\(value) lines of code"让 VoiceOver 读出带单位的值,避免只播报裸数字。
(16:02)如果提供网格线位置,Audio Graphs 会在播放和交互时用触觉反馈表示网格线。演讲示例没有 X 轴网格线,所以传入空数组。
6. 把数据系列交给 AXChartDescriptor
(16:55)坐标轴完成后,还要提供数据系列。每个系列用一个 AXDataSeriesDescriptor 描述。最后把标题、摘要、坐标轴和系列组合成 AXChartDescriptor。
public var accessibilityChartDescriptor: AXChartDescriptor? {
get {
let xAxis = AXNumericDataAxisDescriptor( … )
let yAxis = AXNumericDataAxisDescriptor( … )
let series = AXDataSeriesDescriptor( … )
return AXChartDescriptor(title: model.title,
summary: model.summary,
xAxis: xAxis,
yAxis: yAxis,
additionalAxes: [],
series: [series])
}
set {}
}
关键点:
xAxis是 X 轴描述对象。yAxis是 Y 轴描述对象。series是数据系列描述对象。AXDataSeriesDescriptor需要系列名称、是否连续、以及实际数据点。- 视觉上用点或柱表示的离散数据,
isContinuous应传false。 - 视觉上用线表示的连续数据,
isContinuous应传true。 AXChartDescriptor.title使用图表标题。summary使用模型中的摘要,给用户提供图表结论。additionalAxes: []表示示例没有额外坐标轴。series: [series]把数据系列交给 VoiceOver。
(17:24)演讲建议把模型中的数据映射成 AXDataPoint 数组。这一步让 VoiceOver 拿到图表的真实数值。随后 Audio Graphs 才能播放趋势、读取当前位置的值,并在 Explorer 视图中展示图表详情。
核心启发
1. 给健康趋势图增加 Audio Graphs
- 做什么:为步数、心率、睡眠时长等趋势图增加音频播放和拖动探索。
- 为什么值得做:这类图表的重点是变化趋势和峰值,Audio Graphs 可以把趋势转换成音高变化。
- 怎么开始:整理现有趋势图模型,补充
title、summary、xAxis、yAxis和数据点,然后在图表视图上实现AXChart与accessibilityChartDescriptor。
2. 给理财 App 的资产曲线增加摘要
- 做什么:在资产、预算、支出图表中加入一两句话的图表摘要。
- 为什么值得做:VoiceOver 用户进入 Audio Graph Explorer 前,可以先听到最重要的数据结论。
- 怎么开始:为每张图表生成
summary字段,例如最高值、最低值、增长或下降趋势,再传给AXChartDescriptor(summary:)。
3. 给散点图提供趋势和异常点探索
- 做什么:为散点图提供可播放的数据系列,让用户听出整体上升、下降和异常点。
- 为什么值得做:演讲中的汽车重量和油耗示例展示了散点图也能通过声音发现趋势和离群点。
- 怎么开始:把每个样本映射成
AXDataPoint,用AXDataSeriesDescriptor描述系列。离散点图的isContinuous使用false。
4. 为数据密集图表做区间级 VoiceOver 导航
- 做什么:当图表有大量数据点时,把数据按时间、数值范围或采样区间拆成可导航段落。
- 为什么值得做:逐点导航会让 VoiceOver 用户在几百个元素中来回滑动,体验差,性能也会受影响。
- 怎么开始:在
accessibilityElements中不要直接映射全部原始点,先聚合成合理区间,再为每个区间创建UIAccessibilityElement。
5. 做一套无障碍图表样式开关
- 做什么:集中管理高对比度颜色、符号标记、透明度和动画策略。
- 为什么值得做:图表的视觉可读性直接影响低视力和色觉障碍用户。
- 怎么开始:读取 Differentiate Without Color、Increase Contrast、Reduce Transparency、Reduce Motion 等系统设置,在图表样式层切换颜色、符号和效果。
关联 Session
- Tailor the VoiceOver experience in your data-rich apps — 讲解
AXCustomContent,适合和图表详情的按需朗读一起使用。 - SwiftUI Accessibility: Beyond the basics — 展示 SwiftUI 无障碍的高级做法。
- Support Full Keyboard Access in your iOS app — 让 iOS 应用通过键盘完成导航和操作。
- Create accessible experiences for watchOS — 讲 watchOS 上的无障碍体验设计。
- Accessibility by design: An Apple Watch for everyone — 从产品设计角度理解 Apple 的无障碍方法。
评论
GitHub Issues · utterances