VoiceOver 是
苹果手机上一个给视力不好或者盲人使用应用程序的语音辅助软件 ,
即使你看不见也不成问题。轻触屏幕即可听到你手指划过的内容,然后运用手势来控制设备。VoiceOver 支持 iPhone、iPad 或 iPod touch 所配备的 app。
但是 VoiceOver 并不是万能的,并不能兼容开发者自定义的控件和视图,因此作为开发者,需要通过一些额外的工作让 APP 可以支持无障碍使用。
本教程介绍如何让 APP 可以支持无障碍使用。
无论手机还是模拟器,默认 VoiceOver 都是关闭的,可以在设置里打开 VoiceOver。
手机设备上:设置 / 通用 / 辅助功能 / VoiceOver (iOS9)
打开后,VoiceOver 会朗读屏幕上的项目:
模拟器上:设置 / 通用 / 辅助功能 / Accessibility Inspector
模拟器上 VoiceOver 不会语音提示,只是文字说明。
所有的标准 UIKit 控件和视图默认支持 VoiceOver,当然你也可以通过 UIAccessibility 编程接口关闭或者修改,后面详细说明。
如果 App 使用了用户自定义的一些控件,这时候就需要用户通过 UIAccessibility 编程接口来支持无障碍。首先看一下 UIAccessibility 接口说明。
@interface NSObject (UIAccessibility) //Return YES if the receiver should be exposed as an accessibility element. @property (nonatomic) BOOL isAccessibilityElement; //Returns the localized label that represents the element. @property (nullable, nonatomic, copy) NSString *accessibilityLabel; //Returns a localized string that describes the result of performing an action on the element, when the result is non-obvious. @property (nullable, nonatomic, copy) NSString *accessibilityHint; /* Returns a localized string that represents the value of the element, such as the value of a slider or the text in a text field. Use only when the label of the element differs from a value. For example: A volume slider has a label of "Volume", but a value of "60%". */ @property (nullable, nonatomic, copy) NSString *accessibilityValue; /* Returns a UIAccessibilityTraits mask that is the OR combination of all accessibility traits that best characterize the element. See UIAccessibilityConstants.h for a list of traits. When overriding this method, remember to combine your custom traits with [super accessibilityTraits]. */ @property (nonatomic) UIAccessibilityTraits accessibilityTraits; //Returns the frame of the element in screen coordinates. @property (nonatomic) CGRect accessibilityFrame; ......
从上面可以看出,NSObject 子类都能设置 UIAccessibility,而 NSObject 是所有控件的父类,所以所有的控件都可以设置 UIAccessibility。你可以控制一个控件是否支持 VoiceOver,Label,Hint,Value,Traits 以及控件支持 VoiceOver 的点击区域。
下面先从一个自定义控件例子 (APLCoffeeControl) 来说明 UIAccessibility 的这些属性。
Coffee 控件控制是否显示 coffee 的位置。注意图中右上角的 Accessibility 的参数,和下面控件代码的对应关系。
@interface APLCoffeeControl : UIControl @property (nonatomic, getter = isOn) BOOL on; @end @implementation APLCoffeeControl #pragma mark - UIAccessibilityElement (UIAccessibilityTraitButton) - (BOOL)isAccessibilityElement { return YES; } - (NSString *)accessibilityHint { return @"Show or hide coffee locations"; } - (NSString *)accessibilityLabel { return @"Coffee"; } - (NSString *)accessibilityValue { return ( self.isOn ) ? @"On" : @"Off"; } - (UIAccessibilityTraits)accessibilityTraits { return UIAccessibilityTraitButton; } - (BOOL)accessibilityActivate { self.on = !self.isOn; return YES; }
除了之前提到的那些参数外,这里面还用到了 accessibilityActivate API,它在双击该控件的会执行相应的操作,在这里就是显示或隐藏 coffee 的位置。
@interface NSObject (UIAccessibilityAction) /* Implement accessibilityActivate on an element in order to handle the default action. For example, if a native control requires a swipe gesture, you may implement this method so that a VoiceOver user will perform a double-tap to activate the item. If your implementation successfully handles activate, return YES, otherwise return NO. default == NO */ - (BOOL)accessibilityActivate NS_AVAILABLE_IOS(7_0);
图 3 Accessibility Inspector 中的所有属性并不一定全部显示,视控件 accessibilityTraits 而定。它指定了控件的特性,是一个静态文本呢,还是一个按钮,等等。如下文本控件就没有显示 Hint 和 Value。
/* Accessibility Traits Traits are combined in a mask to help assistive applications understand the meaning and intended use of a particular accessibility element. UIKit applies appropriate traits to all standard controls, however the following traits may be used in conjunction with custom controls. When setting accessiblity traits, combine custom traits with [super accessibilityTraits]. An incorrect combination of custom traits will cause accessibility clients to incorrectly interpret the element. Use common sense when combining traits. */ typedef uint64_t UIAccessibilityTraits; // Used when the element should be treated as a button. UIKIT_EXTERN UIAccessibilityTraits UIAccessibilityTraitButton; // Used when the element should be treated as static text that cannot change. UIKIT_EXTERN UIAccessibilityTraits UIAccessibilityTraitStaticText; /* Used when an element can be "adjusted" (e.g. a slider). The element must also implement accessibilityIncrement and accessibilityDecrement. */ UIKIT_EXTERN UIAccessibilityTraits UIAccessibilityTraitAdjustable NS_AVAILABLE_IOS(4_0); ......
类似于滑动条的 accessibility 是怎么实现的呢?这种控件的特性就是上面列举中的 UIAccessibilityTraitAdjustable,它通过 accessibilityIncrement 和 accessibilityDecrement 这两个 api 来实现增加或减少。如下控制楼层的控件(APLElevatorControl)就是这样的控件。
@interface APLElevatorControl : UIControl @property (nonatomic) NSInteger floor; @end @implementation APLElevatorControl #pragma mark - UIAccessibilityElement (UIAccessibilityTraitAdjustable) - (BOOL)isAccessibilityElement { return YES; } - (NSString *)accessibilityValue { NSNumberFormatter *numberFormatter = [[NSNumberFormatter alloc] init]; [numberFormatter setNumberStyle:NSNumberFormatterDecimalStyle]; return [numberFormatter stringFromNumber:[NSNumber numberWithInteger:self.floor]]; } - (NSString *)accessibilityLabel { return @"Floor"; } - (UIAccessibilityTraits)accessibilityTraits { return UIAccessibilityTraitAdjustable; } - (void)accessibilityDecrement { [self decrementFloor]; // Increase the floor will trigger UI layout changes, so post the notificaiton UIAccessibilityPostNotification(UIAccessibilityLayoutChangedNotification, nil); } - (void)accessibilityIncrement { [self incrementFloor]; // Decrease the floor will trigger UI layout changes, so post the notificaiton UIAccessibilityPostNotification(UIAccessibilityLayoutChangedNotification, nil); }
这里为什么要用到 UIAccessibilityPostNotification 呢?因为这个例子中不同楼层的话,位置分布图会动态变化,而它们又在同一个页面中,通过它告知 VoiceOver 重新加载所有控件的 accessibility 信息,否则 VoiceOver 在识别控件的时候会错误。如果你的程序同一个页面的控件不会动态变化的话,就不需要发这个消息。
/* Posts a notification to assistive applications. Your application might need to post accessibility notifications if you have user interface components that change very frequently or that appear and disappear. */
上面演示的都是基础控件的 accessibility 的实现,如果是自绘控件的话,该怎么实现呢?比如下面这个多边形区域,它是一个厅。UIAccessibilityContainer 这时候就派上用场了。
@interface APLFloorPlanView () @property (strong, nonatomic) NSMutableArray *accessibilityElements; @end @implementation APLFloorPlanView #pragma mark - UIAccessibilityContainer // The content of plan view is generated and presented by custom drawing, rather than by using UIView. // To the objects like rooms and coffee stops to support accessibility, APLFloorPlanView need to // interact with iOS accessibility sytem through UIAccessibilityContainer protocol // Helper method, create Accessibility Elements for the objects in plan view if they not yet exist // and return to the caller. The shape of an accessibility Element is represented by accessibilityPath // - (NSArray *)accessibilityElements { if ( _accessibilityElements == nil ) { _accessibilityElements = [NSMutableArray array]; // Create accessibility elements [[self floorPlanFeatures] enumerateObjectsWithOptions:NSEnumerationReverse usingBlock:^(NSDictionary *floorPlanFeature, NSUInteger idx, BOOL *stop) { UIAccessibilityElement *accessibilityElement = [[UIAccessibilityElement alloc] initWithAccessibilityContainer:self]; accessibilityElement.accessibilityLabel = floorPlanFeature[kFloorPlanString]; accessibilityElement.accessibilityPath = UIAccessibilityConvertPathToScreenCoordinates(floorPlanFeature[kFloorPlanBezierPath], self); [_accessibilityElements addObject:accessibilityElement]; }]; } return _accessibilityElements; } // Accessibility containers MUST return NO to -isAccessibilityElement. - (BOOL)isAccessibilityElement { return NO; } - (id)accessibilityElementAtIndex:(NSInteger)index { return [self.accessibilityElements objectAtIndex:index]; } - (NSInteger)accessibilityElementCount { return self.accessibilityElements.count; } - (NSInteger)indexOfAccessibilityElement:(id)element { return [self.accessibilityElements indexOfObject:element]; }
}
UIAccessiblityContainer 里面的每一项是一个 UIAccessibilityElement,它可以设置 Label、Path,这个例子里 Path 就是多边形的边,只要你在该多边形范围内点击就可以识别出来,实现还是比较方便的。
也可以给一个控件添加额外的 Accessibility 自定义动作 UIAccessibilityCustomAction,如下例子中,控件额外包含两个自定义动作,say hello 和 say Goodbye,选中控件后,上下滑动控制三个动作(两个自定义动作加上控件本身的动作)的切换方向,然后双击控件执行对应动作。
- (AAPLCardView *)addCardViewToView:(UIView *)containerView { AAPLCardView *cardView = [[AAPLCardView alloc] init]; ...... [containerView addSubview:cardView]; UIAccessibilityCustomAction *helloAction = [[UIAccessibilityCustomAction alloc] initWithName:NSLocalizedString(@"Say hello", @"Accessibility action to say hello") target:self selector:@selector(sayHello)]; UIAccessibilityCustomAction *goodbyeAction = [[UIAccessibilityCustomAction alloc] initWithName:NSLocalizedString(@"Say goodbye", @"Accessibility action to say goodbye") target:self selector:@selector(sayGoodbye)]; for (UIView *element in cardView.accessibilityElements) { element.accessibilityCustomActions = @[helloAction, goodbyeAction]; } return cardView;
上面都是通过 API 的方式来设置 Accessibility 参数,还可以通过属性赋值的方式来设置。
UIImageView *logo = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"logo"]]; logo.isAccessibilityElement = YES; logo.accessibilityLabel = NSLocalizedString(@"Hello goodbye, meet your match", @"Logo description");
本教程只是介绍了 Accessibility 的一些基本使用,参考了下列几个例子,更复杂的使用可以参考 Apple 官方文档。