夜雨聆风学习资料网

ARTICLE · 1005322

IOS无障碍解决方案之给转子添加自定义操作

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 无障碍优化:为控件自定义转子导航动作》

了解更多,请参阅:

  1. Apple      Developer Documentation. UIAccessibilityCustomAction. https://developer.apple.com/documentation/uikit/uiaccessibilitycustomaction
  2. Apple      WWDC19. Making Apps More Accessible With Custom Actions (Session      250). https://developer.apple.com/videos/play/wwdc2019/250/
  3. Apple      Developer Documentation. *accessibilityAction(named:_:) - SwiftUI*. https://developer.apple.com/documentation/swiftui/view/accessibilityaction(named:_:)
  4. Apple      Developer Documentation. *accessibilityActions(_:) - SwiftUI*. https://developer.apple.com/documentation/swiftui/view/accessibilityactions(_:)
  5. Apple      Developer Documentation. AccessibilityActionKind. https://developer.apple.com/documentation/swiftui/accessibilityactionkind
  6. Apple      Developer Documentation. editCategory. https://developer.apple.com/documentation/uikit/uiaccessibilitycustomaction/editcategory
  7. Apple      Developer Documentation. *accessibilityActions(category:_:) - SwiftUI*. https://developer.apple.com/documentation/swiftui/view/accessibilityactions(category:_:)
  8. Apple      HIG. Accessibility - VoiceOver. https://developer.apple.com/design/human-interface-guidelines/accessibility#VoiceOver

作者

张赐荣,视障者,信息无障碍软件优化解决方案资深研发工程师。

深耕Web/PC/移动端可访问性工作多年,对跨平台无障碍解决方案拥有独特的理论研究和丰富的实战经验。

精通视障软件交互设计,致力于改善、提升产品可及性体验。

相关学习资料