1905cbb08bc18ce4e693518bdcbfce12
【WWDC21 10122】图表的无障碍支持进阶

作者:Ckitakishi,目前在日本从事 iOS 开发,对动画和图像感兴趣,也关注 Accessibility,正在为写出优美的代码而修炼。

审核:Parsifal,老司机技术周报负责人,微医集团移动诊疗负责人

本文基于 WWDC 10122 - Bring accessibility to charts in your app 创作

前言

Apple 致力于 Accessibility(无障碍辅助功能)并不是一天两天的事情,几乎每年都有超过 5 个 Session 在谈论 Accessibility 亦或是包容性(inclusive);今年 WWDC 首次推出了四个 Digital Lounges,其中之一的主题就是 Accessibility。如果你持续关注 Accessibility 的进展,很容易会发现许多改变都发生于细微之处,我认为这恰恰正是 Apple 令人尊敬的地方,关注细节,不放弃任何用户,日复一日把事情做得更好。

今年,Apple 带来了全新的音频图表(Audio Graphs)来帮助我们提升图表的无障碍体验。不妨就以此为契机,一起来深入探索图表无障碍化的基础知识、现状和新特性。

图表无障碍支持的现状

图表非常有用,它被广泛运用在各个领域,基于其由大量视觉化符号和极少数文字构成的特质,我们往往可以很容易地从中快速获取大量信息,并轻松掌握数据之间的关系和变化趋势,说一图胜万言也不为过。然而,正如 Session 所说:“看不到图表,数据就不存在”,我们必须从头考虑如何让图表在用户缺乏视觉感知时也发挥威力。

如果你曾经涉足于图表的无障碍支持,或许会感到困惑:VoiceOver 不是已经可以帮助我们读出图表中的数据了吗?为什么还要在 WWDC 中旧事重提?倘若这是你第一次接触这个概念,我推荐在 Apple 的股市 App 中体验一下 VoiceOver 的杰出表现。然后你会发现,我们可以与图表进行交互,手指所到之处 VoiceOver 都会为我们读出来。简单来说,图表被划分为了多个区间,每个区间都代表一个可以被无障碍访问的数据点。

当前的图表无障碍支持程度是令人欣喜的,但并不完美,不妨来思考一下这两个问题:

  • 如何快速了解一支股票的涨跌趋势?
  • 如何快速找到该股票的历史最低点或历史最高点?

01-stock-app

01-stock-app

为图表赋予可操作性

尽管不完美,但聊胜于无,让我们暂时忘记上面的两个问题,先来了解一下现阶段该如何为图表赋予可操作性以便用户可以与之进行交互。接下来我们将围绕一张表示咖啡量(杯)与代码量(行)关系的柱状图展开讨论,不得不说,我十分喜欢 Accessibility 团队的例子,数据纯属虚构,但冷幽默让人无法拒绝。

02-coffee-loc

02-coffee-loc

柱状图中的每一条立柱表示一个数据点,X 轴和 Y 轴的数据分别代表咖啡量和代码量。正如上一个部分提到的,图表由众多数据点构成,所以首先需要在 ChartModel 中定义数据点的集合:

class ChartView: UIView {
    let model: ChartModel
}

struct ChartModel {
    let title: String
    let dataPoints: [DataPoint]

    struct DataPoint {
        let name: String
        let x: Double
        let y: Double
    }
}

之后,便可以开始着手为 ChatView 实现无障碍支持。为了简化场景,假设上述的柱状图是基于 Core Graphics 绘制而成,也就是说,柱状图内部的所有元素都不是 UIView 的子类,只是一些线条和图形。显然,它们不会自动获得无障碍属性。

因此,在正式开始实现无障碍支持之前,还需要了解一个名为 UIAccessibilityContainer 的非正式协议,它与 UIAccessibility 一样,都是 UIKit Accessibility 最重要的组成部分。这个协议使得 UIView(或它的子类)所包含的任意对象可以被作为独立个体单独访问,即便那些对象与 UIKit 毫无关系,这样一来,我们便可以将柱状图中的每个柱体当作抽象的视图并为之赋予无障碍属性。

让我们开始吧!首先,要明确 ChatView 的容器类型。这是一个 UIAccessibilityContainerType 类型的枚举值,其中一些成员只能用于特定容器(该枚举从 iOS 11 开始被导入,你可以在 WWDC2017 Session 215 中找到当时的介绍)。根据定义,为了让 VoiceOver 帮助我们读出包括 accessibilityLabel 在内的设置在容器上的无障碍属性 ,需要将容器类型设为 semanticGroup,如下所示:

@available(iOS 11.0, *)
public enum UIAccessibilityContainerType : Int {
    case none = 0
    case dataTable = 1 // 使用该类型的同时务必实现 UIAccessibilityContainerDataTable 协议
    case list = 2
    case landmark = 3

    @available(iOS 13.0, *)
    case semanticGroup = 4 // 辅助功能会查询设置在容器上的无障碍属性,例如accessibilityLabel,以便向用户输出相关信息
}

extension ChartView {
    public override var accessibilityContainerType: UIAccessibilityContainerType { 
        get {
            return .semanticGroup
        }
        set { }
    }
}

然后,我们需要为图表设置一个无障碍标签,以告诉 VoiceOver 当聚焦到图表时该做出什么样的描述。通常返回标题即可,当然也可以是其他简短的文字。但要注意,它需要有独特性,让用户能够第一时间知道自己正在阅读什么:

extension ChartView {
    public override var accessibilityLabel: String? { 
        get {
            return self.model.title
        }
        set { }
    }
}

最后也是最重要的一步,让我们来把数据点一一转换为支持无障碍访问的元素。这并不复杂,一方面我们需要为无障碍数据点设置一个经过本地化的字符串,这样一来,当聚焦到该数据点时 VoiceOver 便会为我们读出当前值,例如:2 杯咖啡 20 行代码;另一方面,因为柱体只是虚拟的视图,所以还需要为它设置一个相对于容器的 frame,这样 VoiceOver 才会知道这些柱体的位置和尺寸:

extension ChartView {
    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 = CGRect(...)
                return axElement
            }
        }
        set {}
    }
}

是不是很简单?只需设置屈指可数的几个无障碍属性就能够获得这么多的好处。这里还有一个小建议:在股市 App 中,以日为单位的话,一支股票上市超过一年会有超过 200 个数据点,将每个数据点都转化为无障碍元素是不切实际的。合理地将数据点分组,以组的名义来创建无障碍元素是非常机智且有必要的。

音频图表

数据可听化(Data Sonification)

还记得前面提过的两个问题吗,你是否有了什么头绪?乍一看,除非添加额外的功能来帮助获取趋势和极值点,否则耐心听完所有可用数据应该是唯一的方法。而音频图表的诞生,能够让我们在解决这些问题的路上迈出跨越性的一步。

让我们先从一个简单的例子开始吧。Accessiblity 团队提供了一个表示年度出生率的折线图案例,通过转子选中音频图表并切换到图表详细之后,可以播放出生率的变化趋势,你没有看错,趋势和声音这两个原本毫不相关的概念竟然被结合到了一起。原理在于,VoiceOver 会将 Y 轴的数值大小转换为音调的高低,这样一来,当听完图表的音调变化之后,用户就可以很清晰地知道出生率的变化趋势了。

03-birth-rates

03-birth-rates

关于第二个问题,如何获取最大值呢?事实上,音频图表还提供了交互模式,双击图表,长按并沿 X 轴移动手指,在听到最高音调时停下来,稍微等待,VoiceOver 就会为我们读出当前坐标所对应的年份和出生率值。

音频图表非常有用,不过这并不是一个旷古绝今的创造,它是数据可听化的优秀实践。数据可听化这个概念贯穿我们生活的点点滴滴,除了音调之外,常常还会用到音量,音色,音长等特征。

图表的概要与特征

你或许已经注意到,在音频图表的下方出现了概要(Summary)和特征(Feature)。这些文字描述是 Apple 基于图表的数据和特征自动生成的,其中了包含图表的形状、趋势和异常值等。尽管这个功能目前只被轻描淡写地提起,我相信随时光更迭,它必将越来越有用。

04-summary

04-summary

AXChart Protocol

在了解音频图表之后,改进上述咖啡 vs 代码柱状图的机会终于来了!简直令人迫不及待。

首先,我们需要为 ChartModel 添加几个新属性,从上一小节的叙述中可以推测出来,音频图表在数据点之外,更多地关注了图表整体。因此,需要添加几个有关坐标系和概要信息的属性:

```swift
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
}

}

top Created with Sketch.