最近做了一些微不足道的无障碍工作,简单做个总结,基本上都是很简单的,很多控件是系统自带旁白,不需要过多的操作,但是有些地方还是需要注意,复杂的页面和控件需要我们做一些处理,无障碍适配的第一原则是可用,能够让有障碍的人士顺畅的使用我们的 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 会按照阅读顺序从左往右阅读,但是我们的子控件很可能是分块的,我想将两个元素合在一起读出来,并且我希望控制这些元素的读取顺序,那么可以使用分组的方式,控制区域内容的读取顺序。

bce02f2c-ccd3-4cae-a8fd-22601abc4cb0
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,我们希望读的时候一次性读出一个完整的元素,标题加上标签以及作者名字,但是用户又可以控制是点进去文章页面还是进入标签页面,该如何实现?

image-20210325144307444

我们将整个 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)
        }

延迟0.1秒的原因可以参考这个回答:Why is UIAccessibility.post(notification: .announcement, argument: “arg”) not announced in voice over?

2.弹出 alert 的时候背景 view 关闭旁白触发

        alert.accessibilityViewIsModal = true

3.支持双指搓擦返回

如果自己自定义导航栏,就会失去系统自带的双指搓擦返回,想要重新添加,只需要在 ViewController 中:

    override func accessibilityPerformEscape() -> Bool {
        self.pop()
        return true
    }

参考文档:

iOS 无障碍编程指南

Accessibility for iOS and tvOS

Accessibility Programming Guide for iOS