轻舟
navigation
✒️

无障碍适配总结

最近做了一些无障碍适配工作,总结一下,基本上都是很简单的,很多控件是系统自带旁白,不需要过多的操作,但是有些地方还是需要注意,复杂的页面和控件需要我们做一些处理,无障碍适配的第一原则是可用,能够让有障碍的人士顺畅的使用我们的 app,第二就是简洁高效,将复杂的页面层级尽量变得简单,能够让旁白快速的识别并且读取。

简单交互

首先还是要熟悉一下视障人士的交互行为。
  • 双指上滑回到当前页面第一个元素
  • 左滑右滑切换前一个后一个元素
  • 三指上下滑上一页下一页
  • 双指写一个 Z 字返回上一页
  • 单指从屏幕底部上滑悬停一下回到主屏幕
  • 选中元素单指下滑切换不同的操作
emmm...太多了也记不住,基本上知道以上这些差不多就可以开始开发了。

基础操作

无障碍化相关的属性都是加在NSObject上的,也就是说,页面上的每个元素都具有这些属性,包括UIWindow,UIControl。
首先让我们看一下 apple 提供的一些基础的(常用的)属性:
// UIAccessibility 提供的属性和方法 /* 控件是否可读 默认no UIKit controls == YES */ open var isAccessibilityElement: Bool /* 旁白读取的内容,不要带标签,例如一个按钮,只需要设置按钮代表的意义,不要这样设置“播放按钮” 默认是 nil,但是如果是 UIKit controls,就会自动识别他的 title */ open var accessibilityLabel: String? /* 旁白读出来的提示,label 如果不清晰,这个就是更详细的解释说明,例如一个按钮,点击之后的操作是下载,那么我们可以设置 Hint 为下载的具体内容 */ open var accessibilityHint: String? /* 代表值,例如滑块的进度 */ open var accessibilityValue: 字符串? /* 控件特征,系统控件默认已经设置了,比如按钮,图片这种 */ open var accessibilityTraits.UIAccessibilityTraits: UIAccessibilityTraits /* 将其中包含的所有可访问元素标记为隐藏,也就是旁白会忽略该元素及其子元素 默认 == NO */ @available(iOS 5.0, *) open var accessibilityElementsHidden: Bool /* 是不是聚焦视图,设置了之后,其余的蒙层就不可以被点击了 */ @available(iOS 5.0, *) open var accessibilityViewIsModal: Bool /* 是否将子视图的可访问元素合并在一起,设置为 true 之后将会把子元素合并成一个整体读取内容 default == NO */ @available(iOS 6.0, *) open var shouldGroupAccessibilityChildren: Bool ---------------------------------------------------------------------------------------- /* 当前是否打开旁白 */ @available(iOS 4.0, *) public static var isVoiceOverRunning: Bool { get }v /* 打开关闭旁白的通知 */ @available(iOS 11.0, *) public static let voiceOverStatusDidChangeNotification: NSNotification.Name ---------------------------------------------------------------------------------------- // 可以读取的子元素的个数 open func accessibilityElementCount() -> Int // 获取第几个能够读取的子元素 open func accessibilityElement(at index: Int) -> Any? open func index(ofAccessibilityElement element: Any) -> Int // 子元素数组 // default == nil @available(iOS 8.0, *) open var accessibilityElements: [Any]?
VoiceOver的逻辑是,递归遍历当前页面的所有元素。如果一个元素isAccessibilityElement == YES,那么读出这个元素的accessibilityLabel等内容。如果一个元素isAccessibilityElement == NO,那么按照它的accessibilityElements 内容遍历其子元素,如果没有设 accessibilityElements ,按照当前系统语言的一般顺序(汉语和英语都是从左到右从上到下)。
那么简单来说就是:
  • 如果一个元素需要被直接读出来,isAccessibilityElement设为YES,accessibilityLabel写入合适的文本
  • 如果一个元素不要读出来,但是它的子元素需要读出来,需要把当前元素isAccessibilityElement设为NO,需要读出来的子元素参考前一条
  • 如果需要控制VoiceOver遍历的顺序,设置accessibilityElements
单个元素
对于基础的控件,我们可以设置isAccessibilityElement属性来控制它是否可以点击:
score.isAccessibilityElement = true score.accessibilityLabel = "score: \\(currentScore)" score.accessibilityHint = "Your current score"
以上是单个元素的无障碍适配。
多个元素组合
如果一个view有多个子的控件,VoiceOver 会按照阅读顺序从左往右阅读,但是我们的子控件很可能是分块的,我想将两个元素合在一起读出来,并且我希望控制这些元素的读取顺序,那么可以使用分组的方式,控制区域内容的读取顺序。
notion image
var elements = [UIAccessibilityElement]() let groupedElement = UIAccessibilityElement(accessibilityContainer: self) groupedElement.accessibilityLabel = "\\(nameTitle.text!), \\(nameValue.text!)" groupedElement.accessibilityFrameInContainerSpace = nameTitle.frame.union(nameValue.frame) elements.append(groupedElement)
添加自定义操作
有一种情况,我们希望读出一个完整的元素,但是又想要用户可以做一些细致的操作,举个例子,下面是一个 cell,我们希望读的时候一次性读出一个完整的元素,标题加上标签以及作者名字,但是用户又可以控制是点进去文章页面还是进入标签页面,该如何实现?
notion image
我们将整个 cell 设置成可以点击,旁白在读的时候会自动识别内部的文字内容并且读出来,这样就可以直接读出文章标题以及相关的信息。
func setupSubviews() { self.isAccessibilityElement = true }
旁白用户的操作习惯是左滑切换到上一个 cell,右滑切换到下一个 cell,在当前选中元素时,上下滑切换控制事件,要让用户可以选择左下角的标签打开标签相关文章,右下角的作者名称打开作者首页,我们可以利用 accessibilityCustomActions 来解决这个问题,我们在数据加载完成之后,给 cell 设置 accessibilityCustomActions,如下:
func updateCell(_ article:Article) { self.accessibilityCustomActions = [ UIAccessibilityCustomAction(name: "打开\\(article.tag)相关文章", target: self, selector: #selector(tagButtonClick)), UIAccessibilityCustomAction(name: "打开\\(article.author)的主页", target: self, selector: #selector(authorNameButtonClick))] }
用户在左右滑选中当前的 cell 时,再上下滑,就可以切换动作事件,选择是打开文章还是打开标签页了。

其他用法

1.弹出 Toast 提示
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { UIAccessibility.post(notification: .announcement, argument: message) }
2.弹出 alert 的时候背景 view 关闭旁白触发
alert.accessibilityViewIsModal = true
3.支持双指搓擦返回
如果自己自定义导航栏,就会失去系统自带的双指搓擦返回,想要重新添加,只需要在 ViewController 中:
override func accessibilityPerformEscape() -> Bool { self.pop() return true }
 
参考文档:
badge