在 VoiceOver 操作转子中添加自定义动作
Custom action:一个少有人知的无障碍 API
UIAccessibilityCustomAction 很早就有了。但很多 iOS 无障碍开发者并不知道这个 API。
该 API 主要用来给一个元素挂上无障碍自定动作,这些动作不会成为独立的 VoiceOver 焦点,而是收纳到“操作”转子项目中,使用者需要的时候,焦点在有操作的元素上,双指旋转到“操作”转子,上下滑动就可以找到这些动作。
就这一个简简单单的 API,却能极大提升 VoiceOver 用户的使用效率。
自绘浮层菜单的无障碍适配困境
比如一个聊天消息列表就是一个特别典型的场景。
一条消息,除了“查看详情”这个主要交互之外,通常还有“复制”、“转发”、“删除”、“引用回复”等一堆附加操作。
健视用户的交互通常是长按消息 → 弹出一个无障碍适配不佳的自绘功能表浮层 → 点击“复制”。
这个流程在视觉上很顺畅,长按的位置就是功能表出现的位置,视觉焦点不需要移动。
但 VoiceOver 用户使用同样方法,体验却截然不同。
首先说长按这个手势本身。VoiceOver 使用者的长按方式和明眼人不太一样,视障使用者需要先按两下按住不放,等系统识别为长按。这个过程中,时长、压力、手指稳定都有要求。按短了变成“聚焦”,按长了可能触发双击激活,稍微抖一下,就会出错。
然後是功能表浮层的问题。长按触发弹出的功能表是一系列新的 UI 元素,对於障碍使用者来说,容易打破操作预期,功能表浮层出现时,VoiceOver 需要把焦点从原聚焦控制项瞬间移进浮层。这个改变过程,在很多自定非标功能表中,会出现失焦问题,用户听到的朗读突然中断,要麽焦点没有进入浮层,或者焦点没有停留在第一个可操作功能表项目上,使用者需要自己摸索当前在哪,更甚整个浮层存在焦点穿透问题,也就是使用者能流览到功能表浮层以外的当前不可操作的内容,复杂的焦点管理,让自定功能表适配无障碍困难重重。
还有一个问题,VoiceOver 使用者流览萤幕时,手指在元素上滑动,系统逐个朗读内容。长按功能表是一个“中断”行为,用户在流览过程中突然被要求处理一个浮层,处理完了还要手动把焦点移回之前的位置继续流览。这个中断成本比视觉使用者高得多,因为视觉使用者可以看到功能表出现,依然知道“我原来在看哪一条消息”。
面对自定功能表在无障碍下的体验问题,很多开发者的第一反应就是,“不用长按了,把操作作为按钮直接平铺到焦点序列中。”
於是每条消息後面跟了三个按钮:“复制”“转发”“删除”。VoiceOver 用户单指左右滑动的时候,会依次经过:
消息内容 → 复制按钮 → 转发按钮 → 删除按钮 → 下一条消息内容 → 复制按钮 → 转发按钮 → 删除按钮 …
这么做确实很简单,但却是灾难级的体验。
十条消息,光这些操作按钮就有三十个焦点。使用者想从第一条消息滑到最後一条,中间要经过三十个“复制”“转发”“删除”。而且这些按钮朗读起来完全一样,一不小心滑过头,又得倒回去重新找。
转子操作的自定动作则用一个非常优雅的方法,解决了这个问题,在不增加 VoiceOver 焦点数量的前提下,给用户提供多个操作入口。
自定动作
UIAccessibilityCustomAction 的设计是,不把关联动作,如“复制”“转发”“删除”变成独立焦点,而是把它们挂在消息内容这个元素上,作为“操作转子”里的可选操作。
使用者的实际操作路径是这样的:
- 单指滑到消息内容 → 听到“张三:明天下午三点开会”
- 双指旋转 VoiceOver 转子,切到“操作”
- 单指上下滑动 → 听到“复制”“转发”“删除”
- 听到“复制”,按两下执行
对比长按功能表的路径:
- 滑到消息内容
- 单指双击并长按,等待系统识别手势
- 菜单浮层出现,焦点跳入浮层
- 在浮层里滑动找“复制”
- 找到後按两下执行
自定动作省掉了“等待浮层出现”和“焦点在浮层里盲找”两步。而且整个操作过程中,焦点始终在消息本身,没有被打断,用户知道自己在操作什吗。
UIKit 实现
UIAccessibilityCustomAction 的构造非常轻量。
let copyAction = UIAccessibilityCustomAction(
name: "复制"
) { _ in
self.copyMessage()
return true
}
let forwardAction = UIAccessibilityCustomAction(
name: "转发"
) { _ in
self.forwardMessage()
return true
}
let deleteAction = UIAccessibilityCustomAction(
name: "删除"
) { _ in
self.deleteMessage()
return true
}
cell.accessibilityCustomActions = [copyAction, forwardAction, deleteAction]
闭包里的 return true 表示“这个操作我处理了”。如果返回 false,则表示没有处理动作。
多个动作按顺序在操作转子中展示。VoiceOver 使用者上下滑动时依序流览。
还有一个相关的方法是 accessibilityActivate。它重写的是 VoiceOver 使用者按两下元素时的默认动作。
override func accessibilityActivate() -> Bool {
// 按两下这条消息时,预设执行查看详情
self.openMessageDetail()
return true
}
accessibilityCustomActions 是追加到系统内置动作之后的,不是替换,系统原有的Actions依然会存在。
SwiftUI 实现
SwiftUI 的修饰器写法更简洁。
Text(message.content)
.accessibilityAction(named: "复制") {
copyMessage(message)
}
多个动作:
MessageView(message)
.accessibilityActions {
Button("复制") { copyMessage(message) }
Button("转发") { forwardMessage(message) }
Button("删除") { deleteMessage(message) }
}
每个 Button 的标题会成为操作转子里的动作名称,action 闭包负责执行。
SwiftUI 还提供了一些系统预定义的动作类型,用 AccessibilityActionKind 表示:
.accessibilityAction(.delete) {
deleteMessage()
}
.delete、.escape 这些 API 使用系统定义的 accessibility action semantics 标准化动作语义,其名称和呈现由系统/辅助技术处理,通常不需要自己定义对应的本地化动作名称。
iOS 新系统的功能
当自定动作太多的时候,操作转子本身也会变成一个长列表。iOS 18 引入了Category来解决这个问题。
UIKit 里给 action 加一个 category 属性:
let action = UIAccessibilityCustomAction(name: "重命名") { ... }
action.category = UIAccessibilityCustomAction.editCategory
SwiftUI 里用专门的修饰器:
EditorView()
.accessibilityActions(category: .edit) {
Button("重命名") { ... }
Button("删除") { ... }
}
editCategory 为VoiceOver提供信息,开发者可将具有编辑语义的动作组织归类到编辑转子中。
总之,主要、高频、需要快速发现的操作,应考虑作为独立的 accessibility element;次要、低频、与当前元素紧密相关的操作,则非常适合使用 custom actions。
参考
有关如何在 Android 中实现类似功能,可参阅 《Android TalkBack 无障碍优化: 为控件自定义转子导航动作》
了解更多,请参阅:
-
Apple Developer Documentation. UIAccessibilityCustomAction. https://developer.apple.com/documentation/uikit/uiaccessibilitycustomaction
-
Apple WWDC19. Making Apps More Accessible With Custom Actions (Session 250). https://developer.apple.com/videos/play/wwdc2019/250/
-
Apple Developer Documentation. accessibilityAction(named:_😃 - SwiftUI. https://developer.apple.com/documentation/swiftui/view/accessibilityaction(named:_😃
-
Apple Developer Documentation. accessibilityActions(_😃 - SwiftUI. https://developer.apple.com/documentation/swiftui/view/accessibilityactions(_😃
-
Apple Developer Documentation. AccessibilityActionKind. https://developer.apple.com/documentation/swiftui/accessibilityactionkind
-
Apple Developer Documentation. editCategory. https://developer.apple.com/documentation/uikit/uiaccessibilitycustomaction/editcategory
-
Apple Developer Documentation. accessibilityActions(category:_😃 - SwiftUI. https://developer.apple.com/documentation/swiftui/view/accessibilityactions(category:_😃
-
Apple HIG. Accessibility - VoiceOver. https://developer.apple.com/design/human-interface-guidelines/accessibility#VoiceOver
作者
张赐荣,视障者,信息无障碍软件优化解决方案资深研发工程师。
深耕Web/PC/移动端可访问性工作多年,对跨平台无障碍解决方案拥有独特的理论研究和丰富的实战经验。
精通视障软件交互设计,致力于改善、提升产品可及性体验。
知乎: @张赐荣
赐荣博客: www.prc.cx
浙公网安备 33010602011771号