-
前言
近期处理VoiceOver适配问题时,在复杂界面处理遇到了一些棘手的问题,VoiceOver问题可参考的资料不多,所以我把自己踩过的坑记录如下。
本文将直接从开发的角度切入,不再介绍VoiveOver的实际意义。
本文描述操作路径的版本:
1 2
| iPhone 12 Pro / iOS 14.7.1 iOS 14.5 Frameworks - UIKit
|
一、VoiceOver 操作指南

再补充两个比较常用的手势:
- 切换App:四指滑动
- 拖动手势:单指双击并按住,拖动即可。

手机开启了VoiceOver后,初次接触,会有几个比较高频遇到的问题:
1. 怎么再关闭 VoiceOver(如何给VoiceOver设置开关快捷键)
我们知道,打开VoiceOver的方式是:
设置 - 辅助功能 - 旁白 - 打开
但作为初次使用VoiceOver的问题是,使用手势切到了一个页面卡住了,不知道该如何返回,这时着急取消VoiceOver。
所以作为 VoiceOver 开发者,第一步你要做的是开启 「旁白 - 辅助功能快捷键」,步骤为:
设置 - 辅助功能 - 辅助功能快捷键 - 旁白
设置完毕后,你就可以连续按住手机侧边按钮3次实现 开/关 旁白VoiceOver 的功能。
2. 打开VoiceOver后,怎么切换到开发的Dev_App
方法一(建议):
配置好 「旁白 - 辅助功能快捷键」,打开 Dev_App ,连按3次侧边按钮,打开旁白
方法二:
在设置中打开Voice,四指滑动屏幕,切换App。
3. 打开VoiceOver后,屏幕锁屏了,怎么在VoiceOver状态下解锁
方法一:
如果你配置了「旁白 - 辅助功能快捷键」,那么连按3次侧边按钮,关闭VoiceOver,正常打开屏幕即可
方法二:
如果你没有配置「旁白 - 辅助功能快捷键」,那么可以单指上滑后停止3秒钟,松手即可打开屏幕。

4. 打开VoiceOver后,不小心把控制面板拉下来了,怎么恢复控制面板?
和问题3解决方法一样。
二、开发指南
(一) UIAccessibility API 解读
iOS中处理VoiceOver的关键类是UIAccessibility.h
,相当于 VoiceOver 的 API,
抹去 UIAccessibility.h
中各项注释,我们得到如下内容:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114
| @interface NSObject (UIAccessibility)
@property (nonatomic) BOOL isAccessibilityElement;
@property (nullable, nonatomic, copy) NSString *accessibilityLabel;
@property (nullable, nonatomic, copy) NSAttributedString *accessibilityAttributedLabel API_AVAILABLE(ios(11.0),tvos(11.0));
@property (nullable, nonatomic, copy) NSString *accessibilityHint;
@property (nullable, nonatomic, copy) NSAttributedString *accessibilityAttributedHint API_AVAILABLE(ios(11.0),tvos(11.0));
@property (nullable, nonatomic, copy) NSString *accessibilityValue;
@property (nullable, nonatomic, copy) NSAttributedString *accessibilityAttributedValue API_AVAILABLE(ios(11.0),tvos(11.0));
@property (nonatomic) UIAccessibilityTraits accessibilityTraits;
@property (nonatomic) CGRect accessibilityFrame;
UIKIT_EXTERN CGRect UIAccessibilityConvertFrameToScreenCoordinates(CGRect rect, UIView *view) API_AVAILABLE(ios(7.0));
@property (nullable, nonatomic, copy) UIBezierPath *accessibilityPath API_AVAILABLE(ios(7.0));
UIKIT_EXTERN UIBezierPath *UIAccessibilityConvertPathToScreenCoordinates(UIBezierPath *path, UIView *view) API_AVAILABLE(ios(7.0));
@property (nonatomic) CGPoint accessibilityActivationPoint API_AVAILABLE(ios(5.0));
@property (nullable, nonatomic, strong) NSString *accessibilityLanguage;
@property (nonatomic) BOOL accessibilityElementsHidden API_AVAILABLE(ios(5.0));
@property (nonatomic) BOOL accessibilityViewIsModal API_AVAILABLE(ios(5.0));
@property (nonatomic) BOOL shouldGroupAccessibilityChildren API_AVAILABLE(ios(6.0));
@property (nonatomic) UIAccessibilityNavigationStyle accessibilityNavigationStyle API_AVAILABLE(ios(8.0));
@property (nonatomic) BOOL accessibilityRespondsToUserInteraction API_AVAILABLE(ios(13.0),tvos(13.0));
@property (null_resettable, nonatomic, strong) NSArray<NSString *> *accessibilityUserInputLabels API_AVAILABLE(ios(13.0),tvos(13.0));
@property (null_resettable, nonatomic, copy) NSArray<NSAttributedString *> *accessibilityAttributedUserInputLabels API_AVAILABLE(ios(13.0),tvos(13.0));
@property(nullable, nonatomic, copy) NSArray *accessibilityHeaderElements UIKIT_AVAILABLE_TVOS_ONLY(9_0);
@property(nullable, nonatomic, strong) UIAccessibilityTextualContext accessibilityTextualContext API_AVAILABLE(ios(13.0), tvos(13.0));
@end
@interface NSObject (UIAccessibilityFocus)
- (void)accessibilityElementDidBecomeFocused API_AVAILABLE(ios(4.0)); - (void)accessibilityElementDidLoseFocus API_AVAILABLE(ios(4.0));
- (BOOL)accessibilityElementIsFocused API_AVAILABLE(ios(4.0));
- (nullable NSSet<UIAccessibilityAssistiveTechnologyIdentifier> *)accessibilityAssistiveTechnologyFocusedIdentifiers API_AVAILABLE(ios(9.0));
UIKIT_EXTERN __nullable id UIAccessibilityFocusedElement(UIAccessibilityAssistiveTechnologyIdentifier __nullable assistiveTechnologyIdentifier) API_AVAILABLE(ios(9.0));
@end
@interface NSObject (UIAccessibilityAction)
- (BOOL)accessibilityActivate API_AVAILABLE(ios(7.0));
- (void)accessibilityIncrement API_AVAILABLE(ios(4.0)); - (void)accessibilityDecrement API_AVAILABLE(ios(4.0));
typedef NS_ENUM(NSInteger, UIAccessibilityScrollDirection) { UIAccessibilityScrollDirectionRight = 1, UIAccessibilityScrollDirectionLeft, UIAccessibilityScrollDirectionUp, UIAccessibilityScrollDirectionDown, UIAccessibilityScrollDirectionNext API_AVAILABLE(ios(5.0)), UIAccessibilityScrollDirectionPrevious API_AVAILABLE(ios(5.0)), };
- (BOOL)accessibilityScroll:(UIAccessibilityScrollDirection)direction API_AVAILABLE(ios(4.2));
- (BOOL)accessibilityPerformEscape API_AVAILABLE(ios(5.0));
- (BOOL)accessibilityPerformMagicTap API_AVAILABLE(ios(6.0));
@property (nullable, nonatomic, strong) NSArray <UIAccessibilityCustomAction *> *accessibilityCustomActions API_AVAILABLE(ios(8.0)); @end
@protocol UIAccessibilityReadingContent @required
- (NSInteger)accessibilityLineNumberForPoint:(CGPoint)point API_AVAILABLE(ios(5.0));
- (nullable NSString *)accessibilityContentForLineNumber:(NSInteger)lineNumber API_AVAILABLE(ios(5.0));
- (CGRect)accessibilityFrameForLineNumber:(NSInteger)lineNumber API_AVAILABLE(ios(5.0));
- (nullable NSString *)accessibilityPageContent API_AVAILABLE(ios(5.0));
@optional - (nullable NSAttributedString *)accessibilityAttributedContentForLineNumber:(NSInteger)lineNumber API_AVAILABLE(ios(11.0), tvos(11.0)); - (nullable NSAttributedString *)accessibilityAttributedPageContent API_AVAILABLE(ios(11.0), tvos(11.0));
@end
@interface NSObject(UIAccessibilityDragging)
@property (nullable, nonatomic, copy) NSArray<UIAccessibilityLocationDescriptor *> *accessibilityDragSourceDescriptors API_AVAILABLE(ios(11.0)) API_UNAVAILABLE(watchos, tvos);
@property (nullable, nonatomic, copy) NSArray<UIAccessibilityLocationDescriptor *> *accessibilityDropPointDescriptors API_AVAILABLE(ios(11.0)) API_UNAVAILABLE(watchos, tvos);
@end
|

通过阅读UIAccessibility.h
我们可以发现这个类针对 NSObject
新增了4个Category,声明了一个 Notification:
- UIAccessibility :VoiceOver基本设置的属性
- UIAccessibilityFocus :VoiceOver聚焦相关逻辑
- UIAccessibilityAction :处理和 VoiceOver 的手势冲突问题
- UIAccessibilityPostNotification : 抛VoiceOver的通知,控制VoiceOver行为
1. UIAccessibility
UIAccessibility 这个 Category 的官方解释为:
1 2 3 4 5 6 7 8 9 10 11
| UIAccessibility is implemented on all standard UIKit views and controls so that assistive applications can present them to users with disabilities.
Custom items in a user interface should override aspects of UIAccessibility to supply details where the default value is incomplete.
For example, a UIImageView subclass may need to override accessibilityLabel, but it does not need to override accessibilityFrame.
A completely custom subclass of UIView might need to override all of the UIAccessibility methods except accessibilityFrame.
|
重点关注最后一句话:一个完全自定义的view可能需要覆写UIAccessibility这个Category中除了accessibilityFrame之外的所有方法。
可见 UIAccessibility 这个 Category 在 VoiceOver 中的重要性。
(1) isAccessibilityElement (property) ⭐️⭐️⭐️
作为 VoiceOver 最关键的一个property,只有 isAccessibilityElement 才会响应 VoiceOver 选中事件。
关于 isAccessibilityElement 还有两个非常重要的特性:
- 默认值:UIView 中,只有 UILabel、UIButton 的 isAccessibilityElement 默认是 YES,其余 UIView.isAccessibilityElement 默认是 NO。
- 消息传递:view.superView.isAccessibilityElement 为 YES ,无论 view.isAccessibilityElement 怎么设置,VoiceOver 都将不会响应到 view,而只会响应 view.superView
Tips:关于 VoiceOver 的消息传递,我们在讲完API后会深入探讨。
(2) accessibilityLabel (property) ⭐️⭐️⭐️
当我们选中 View ,VoiceOver 读出的内容就是 accessibilityLabel 的内容。
(3) accessibilityHint (property)⭐️⭐️
一个简短的本地化短语,用于描述作用于元素上的操作所得到的结果。 例如“添加标题”或“查看购物车”
(4)accessibilityValue(property)⭐️⭐️
某UI元素的当前值,并且这个值无法由label表示出来。例如,某个slider的accessibilityLabel可能是“速度”,但其当前值可能为“50%”
(5)accessibilityTraits(property)⭐️⭐️
一个或多个单独特征的组合,每个特征描述元素的某一个方面,包括状态、行为或用法。这些都定义成了UIAccessibilityTrait的某一个常量。
(6) accessibilityFrame (property)⭐️
屏幕中某元素的坐标和大小。这个属性比较重要,但实际开发过程中我们很少会显示的设置accessibilityFrame。
(7) accessibilityPath (property)
手动画贝塞尔曲线决定响应的范围,和 accessibilityFrame 一样,基本用不到,但要知道有这个功能。
(8)accessibilityActivationPoint (property)
当你点击元素时,返回的point,默认值是 accessibilityFrame 的 mid-point。
(9)accessibilityLanguage (property)
VoiceOver 读内容时,使用的语言。常规情况下我们不需要显示地设置accessibilityLanguage,它会根据系统语言读出来。
(10) accessibilityElementsHidden (property)
设置一个view能否响应 VoiceOver ,isAccessibilityElement 是针对当前 View 的,而 accessibilityElementsHidden 是针对当前 View 和其所有 subView 的。
如果我们需要把一个ViewController的根View以及内部所有子View都不支持VoiceOver,可以这么写:
self.view.accessibilityElementsHidden = YES;
VoiceOver 本身的设置体系就比较散,不建议使用 accessibilityElementsHidden ,不然会导致 superView 的设置导致所有的子view都受到影响,可能会影响到其它同学的业务。
慎用。
(11) accessibilityViewIsModal (property)⭐️⭐️
试想这样一个场景:A present 出半屏B,我们预期是 VoiceOver 只在 半屏B 响应,只有关闭了B,VoiceOver 才可以去感知 A。
这时如果把 半屏B.accessibilityViewIsModal 设置为 YES,就可以实现上面的效果。
(12) shouldGroupAccessibilityChildren (property)⭐️
可以通过 shouldGroupAccessibilityChildren 将一系列子view聚合在一起,调整 VoiceOver 响应的顺序。

比如上面这个view,如果我们不做任何调整,VoiceOver 读取的顺序是:
Label 1 -> Label 2 -> Button -> Label 3
如果我们把 label 1、2、3 放到一个父view labelContainer 中,并设置 labelContainer.shouldGroupAccessibilityChildren 为 YES,
那么 VoiceOver 的顺序将调整为:
Label 1 -> Label 2 -> Label 3 -> Button
还没有罗列的属性如下,当你觉得你遇到的问题实在是无法用现有API解决,可以看看下面这些比较偏僻的API:
1 2 3 4 5 6
| accessibilityNavigationStyle accessibilityRespondsToUserInteraction accessibilityUserInputLabels accessibilityAttributedUserInputLabels accessibilityHeaderElements accessibilityTextualContext
|
⭐️⭐️⭐️问题:Traits、label、Value、Hint 都是 NSString 类型,那么如果都设置上,会都读出来吗?读的顺序如何呢?
按下面这个流程设置:
1 2 3 4
| self.likeBtn.accessibilityLabel = @"点赞"; self.likeBtn.accessibilityTraits = UIAccessibilityTraitSelected; self.likeBtn.accessibilityValue = @"99"; self.likeBtn.accessibilityHint= @"给内容点赞";
|
VoiceOver 读出的顺序是: 已选定 - 点赞 - 99 - 给内容点赞,
也即: Traits - label - value - hint
所以如果你只是给view设置一个读音,使用上面任何一个元素都是可以实现的,但要注意的是:
开发应面向未来,应该把读音放到合适的字段,不要错误占用了字段。
2. UIAccessibilityFocus
此 Category 主要用于处理 VoiceOver 下的聚焦问题。
(1) accessibilityElementDidBecomeFocused (method)
成为焦点后的回调。
(2)accessibilityElementDidLoseFocus (method)
失去焦点后的回调。
(3)accessibilityElementIsFocused (method)
获取当前是不是VoiceOver的焦点。
3. UIAccessibilityFocus
(1) accessibilityActivate (method)
单指轻点两次的回调,默认的双击之后会触发的函数。一般可以通过覆写这个方法来解决手势冲突问题。
处理和控制 VoiceOver 滚动方向。
试想这样一个场景:A present 出模态B 视图,当A消失时,如果实现模态B的同时消失?
只需要在模态B中将accessibilityPerformEscape
设置为YES即可。
4. UIAccessibilityPostNotification
UIAccessibilityPostNotification(UIAccessibilityNotifications, view);
1 2 3 4 5 6 7 8 9
| UIAccessibilityNotifications
UIAccessibilityScreenChangedNotification UIAccessibilityLayoutChangedNotification UIAccessibilityAnnouncementNotification UIAccessibilityPageScrolledNotification UIAccessibilityPauseAssistiveTechnologyNotification UIAccessibilityResumeAssistiveTechnologyNotification
|
(1)UIAccessibilityScreenChangedNotification
新元素出现调用,或在 appear 生命周期处调用。
使用 UIAccessibilityPostNotification(UIAccessibilityScreenChangedNotification, view)
可以将 VoiceOver 的光标移动到 view
(2)UIAccessibilityLayoutChangedNotification
触发layout调用。
使用 UIAccessibilityPostNotification(UIAccessibilityLayoutChangedNotification, view)
可以将 VoiceOver 的光标移动到 view
(3)UIAccessibilityAnnouncementNotification
使用 UIAccessibilityPostNotification(UIAccessibilityAnnouncementNotification, @"读出的内容")
,可以读出相应的内容,实现自定义时间点来读出相应内容。