ARTICLE · 1005322
IOS无障碍解决方案之给转子添加自定义操作
在 VoiceOver 操作转子中添加自定义动作
Custom action:一个少有人知的无障碍 API
UIAccessibilityCustomAction很早就有了。但很多 iOS 无障碍开发者并不知道这个API。
该 API 主要用来给一个元素挂上无障碍自定义动作,这些动作不会成为独立的 VoiceOver 焦点,而是收纳到“操作”转子项目中,使用者需要的时候,焦点在有操作的元素上,双指旋转到“操作”转子,上下滑动就可以找到这些动作。
就这一个简简单单的API,却能极大提升VoiceOver 用户的使用效率。
自绘浮层菜单的无障碍适配困境
比如一个聊天消息列表就是一个特别典型的场景。
一条消息,除了“查看详情”这个主要交互之外,通常还有“复制”、“转发”、“删除”、“引用回复”等一堆附加操作。
健视用户的交互通常是长按消息 → 弹出一个无障碍适配不佳的自绘菜单浮层 → 点击“复制”。
这个流程在视觉上很顺畅,长按的位置就是菜单出现的位置,视觉焦点不需要移动。
但 VoiceOver 用户使用同样方法,体验却截然不同。
首先说长按这个手势本身。VoiceOver 使用者的长按方式和明眼人不太一样,视障使用者需要先轻点两下并按住,等系统识别为长按。这个过程中,时长、压力、手指稳定都有要求。按短了变成“聚焦”,按长了可能触发双击激活,稍微抖一下,就会出错。
然后是菜单浮层的问题。长按触发弹出的菜单是一系列新的 UI 元素,对于障碍使用者来说,容易打破操作预期,菜单浮层出现时,VoiceOver 需要把焦点从原聚焦控件瞬间移进浮层。这个改变过程,在很多自定义非标菜单中,会出现失焦问题,用户听到的朗读突然中断,要么焦点没有进入浮层,或者焦点没有停留在第一个可操作菜单项上,使用者需要自己摸索当前在哪,更甚整个浮层存在焦点穿透问题,也就是使用者能浏览到菜单浮层以外的当前不可操作的内容,复杂的焦点管理,让自定义菜单适配无障碍困难重重。
还有一个问题,VoiceOver 使用者浏览屏幕时,手指在元素上滑动,系统逐个朗读内容。长按菜单是一个“中断”行为,用户在浏览过程中突然被要求处理一个浮层,处理完了还要手动把焦点移回之前的位置继续浏览。这个中断成本比视觉使用者高得多,因为视觉使用者可以看到菜单出现,依然知道“我原来在看哪一条消息”。
面对自定义菜单在无障碍下的体验问题,很多开发者的第一反应就是,“不用长按了,把操作作为按钮直接平铺到焦点序列中。”
于是每条消息后面跟了三个按钮:“复制”“转发”“删除”。VoiceOver 用户单指左右滑动的时候,会依次经过:
消息内容 → 复制按钮 → 转发按钮 → 删除按钮 → 下一条消息内容 → 复制按钮 → 转发按钮 → 删除按钮 …
这么做确实很简单,但却是灾难级的体验。
十条消息,光这些操作按钮就有三十个焦点。使用者想从第一条消息滑到最后一条,中间要经过三十个“复制”“转发”“删除”。而且这些按钮朗读起来完全一样,一不小心滑过头,又得倒回去重新找。
转子操作的自定义动作则用一个非常优雅的方法,解决了这个问题,在不增加 VoiceOver 焦点数量的前提下,给用户提供多个操作入口。
自定义动作
UIAccessibilityCustomAction的设计是,不把关联动作,如“复制”“转发”“删除”变成独立焦点,而是把它们挂在消息内容这个元素上,作为“操作转子”里的可选操作。
使用者的实际操作路径是这样的:
1.单指滑到消息内容 → 听到“张三:明天下午三点开会”
2.双指旋转 VoiceOver 转子,切到“操作”
3.单指上下滑动 → 听到“复制”“转发”“删除”
4.听到“复制”,轻点两下执行
对比长按菜单的路径:
1.滑到消息内容
2.单指双击并长按,等待系统识别手势
3.菜单浮层出现,焦点跳入浮层
4.在浮层里滑动找“复制”
5.找到后轻点两下执行
自定义动作省掉了“等待浮层出现”和“焦点在浮层里盲找”两步。而且整个操作过程中,焦点始终在消息本身,没有被打断,用户知道自己在操作什么。
UIKit 实现
UIAccessibilityCustomAction的构造非常轻量。
letcopyAction=UIAccessibilityCustomAction(name:"复制"){ _ inself.copyMessage()returntrue}letforwardAction= UIAccessibilityCustomAction(name:"转发"){ _ inself.forwardMessage()returntrue}letdeleteAction= UIAccessibilityCustomAction(name:"删除"){ _ inself.deleteMessage()returntrue}cell.accessibilityCustomActions =[copyAction, forwardAction, deleteAction]
闭包里的return true表示“这个操作我处理了”。如果返回false,则表示没有处理动作。
多个动作按顺序在操作转子中展示。VoiceOver 使用者上下滑动时依序浏览。
还有一个相关的方法是accessibilityActivate。它重写的是 VoiceOver 使用者轻点两下元素时的默认动作。
overridefuncaccessibilityActivate() -> Bool{// 轻点两下这条消息时,默认执行查看详情self.openMessageDetail()returntrue}
注意: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属性:
letaction= 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/移动端可访问性工作多年,对跨平台无障碍解决方案拥有独特的理论研究和丰富的实战经验。
精通视障软件交互设计,致力于改善、提升产品可及性体验。