Bring accessibility to charts in your app
Accessibility 进阶 20m

为 App 中的图表添加无障碍支持

Bring accessibility to charts in your app

2021年6月11日

在 Apple 官方观看视频

一句话判断

图表是视障用户最大的信息障碍 —— 你不能用一个 accessibilityLabel 说完整个折线图。这集 Session 教你用 Data Sonification(数据声音化)、Audio Graph(音频图表)和分层描述来让图表”可听见”。

这场 Session 讲了什么

Session 专门讲解了如何为图表类可视化内容提供无障碍访问。视觉用户一眼就能看出趋势(上涨、下跌、峰值),但 VoiceOver 用户只能听到一串数字,无法理解数据模式。

苹果提供的解决方案有三个层次:

  1. 概述(Summary):用自然语言描述图表的整体趋势(“过去 30 天的销售数据,整体呈上升趋势,最高点在 6 月 15 日”)。
  2. 数据点导航:用自定义 Rotor 让 VoiceOver 用户逐个浏览数据点,每个点包含数值和时间信息。
  3. Audio Graph(音频图表):iOS 15 新增的 AXChart 协议,用音调变化表达数据趋势 —— 高音代表高值、低音代表低值,随时间播放让用户”听到”数据曲线。

值得深挖的点

Audio Graph 的用户体验。当 VoiceOver 用户在一个图表上执行特定手势(两指轻点并拖动)时,系统会播放一段音频:音调从低到高对应数据从低到高。用户可以通过拖动速度控制播放速度,也可以暂停在某个数据点上听到具体的数值。这个功能的底层依赖 AXChartDescriptor —— 你提供数据点数组,系统生成音频。

分层描述的模板。苹果建议图表的无障碍描述分三层:第一层是 Summary(一句话概括趋势)、第二层是 Data Points(可以逐个浏览的数据列表)、第三层是 Audio Graph(声音化表达)。VoiceOver 用户通过 Rotor 在这三层之间切换。

代码片段

创建 Audio Graph 描述

import Accessibility

// 为图表创建 Audio Graph 描述
func createChartDescriptor(for data: [DataPoint]) -> AXChartDescriptor {
    // 创建 X 轴描述
    let xAxis = AXNumericDataAxisDescriptor(
        title: "日期",
        range: (data.first?.date ?? Date())...(data.last?.date ?? Date()),
        gridlinePositions: [],
        valueDescriptionProvider: { value in
            let date = Date(timeIntervalSince1970: value)
            return date.formatted(date: .abbreviated, time: .omitted)
        }
    )
    // 创建 Y 轴描述
    let yAxis = AXNumericDataAxisDescriptor(
        title: "销售额(美元)",
        range: 0...(data.map(\.value).max() ?? 100),
        gridlinePositions: [],
        valueDescriptionProvider: { value in
            "$\(Int(value))"
        }
    )
    // 创建数据点序列
    let series = AXDataSeriesDescriptor(
        name: "日销售额",
        isContinuous: true,
        dataPoints: data.map { point in
            AXDataPoint(
                x: point.date.timeIntervalSince1970,
                y: point.value,
                additionalValues: [],
                label: nil
            )
        }
    )
    return AXChartDescriptor(
        title: "过去 30 天销售额",
        summary: "整体呈上升趋势,6月15日达到峰值 $\(data.map(\.value).max() ?? 0)",
        xAxis: xAxis,
        yAxis: yAxis,
        series: [series]
    )
}

SwiftUI 中应用图表描述

import SwiftUI

struct AccessibleChartView: View {
    let data: [DataPoint]

    var body: some View {
        ChartView(data: data)
            // 附加图表无障碍描述
            .accessibilityChartDescriptor(createChartDescriptor(for: data))
            // 概述层
            .accessibilityElement(children: .ignore)
            .accessibilityLabel(chartSummary)
            .accessibilityValue("使用两指轻点并拖动来听取音频图表")
            // 自定义 Rotor:按数据点浏览
            .accessibilityRotor("数据点") {
                ForEach(Array(data.enumerated()), id: \.offset) { index, point in
                    AccessibilityRotorEntry(
                        "\(point.date.formatted()): $\(Int(point.value))",
                        id: index
                    ) {
                        // 聚焦到具体数据点
                    }
                }
            }
    }

    private var chartSummary: String {
        let trend = data.last!.value > data.first!.value ? "上升" : "下降"
        let maxVal = data.map(\.value).max()!
        let maxDate = data.first { $0.value == maxVal }!.date
        return "销售额\(trend)趋势。过去30天最高为\(maxDate.formatted(date: .abbreviated, time: .omitted))的 $\(Int(maxVal))"
    }
}

使用 AttributedString 优化数据点描述

import Foundation

func describeDataPoint(_ point: DataPoint, index: Int, total: Int) -> String {
    let dateFormatter = DateFormatter()
    dateFormatter.dateStyle = .medium
    // 提供上下文的描述
    let position: String
    if index == 0 {
        position = "起始点"
    } else if index == total - 1 {
        position = "终点"
    } else {
        position = "第\(index + 1)个点"
    }
    return "\(position): \(dateFormatter.string(from: point.date)), 销售额 \(point.value, specifier: "%.0f") 美元"
}

最佳实践

  1. 图表概述要用自然语言描述趋势,不要罗列数字。“销售额本月持续增长,月中达到峰值”比”100, 120, 135, 180, 165, 140”有用得多。
  2. Audio Graph 是最有力的补充。视障用户反馈说,听到数据曲线比听到任何文字描述都能更快理解趋势。确保你的 AXChartDescriptor 数据点足够密集(至少 20 个点)。
  3. 图表颜色不能是唯一的信息载体。色盲用户看不到红绿对比。用纹理、形状标签或文字标注补充颜色编码。

还有什么值得关注

  • AXChartDescriptor 同时支持折线图、柱状图和散点图,通过 isContinuous 参数区分。
  • iOS 15 的 VoiceOver 新增了”音频图表”手势,用户可以在任何支持 AXChart 的元素上触发。
  • 苹果推荐在图表旁边提供一个”数据表格”视图的切换选项,让用户选择最适合自己的数据消费方式。
WWDC 2021