无障碍适配总结
最近做了一些无障碍适配工作,总结一下,基本上都是很简单的,很多控件是系统自带旁白,不需要过多的操作,但是有些地方还是需要注意,复杂的页面和控件需要我们做一些处理,无障碍适配的第一原则是可用,能够让有障碍的人士顺畅的使用我们的 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 会按照阅读顺序从左往右阅读,但是我们的子控件很可能是分块的,我想将两个元素合在一起读出来,并且我希望控制这些元素的读取顺序,那么可以使用分组的方式,控制区域内容的读取顺序。
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,我们希望读的时候一次性读出一个完整的元素,标题加上标签以及作者名字,但是用户又可以控制是点进去文章页面还是进入标签页面,该如何实现?
我们将整个 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
}
参考文档: