iOS Accessibility易用性 VoiceOver(盲人模式)粗浅适配

4,284 阅读7分钟

一、打开盲人模式: 

手机(6s iOS12.4)路径:设置->通用->辅助功能(底部)->辅助功能快捷键->勾选旁白(voiceOver)


这里需要特别说明一下,在通用列表进入辅助功能列表页时,顶部也有个旁白选项可以打开该功能,但是这里最好还是按照我图示的方式来打开旁白功能。因为正如红框框出来的那样,按照我这种方式打开旁白功能是可以直接快捷键(连按三下home键)开启关闭的,这在你的开发当中提升的效率可不是一点半点,因为盲人模式下的使用方式跟正常差距还是比较大的,比如你要调试一个比较深层级的页面,可以先以正常方式进入到目的页面再按三下home键开启旁白。如果以另外一种方式打开旁白,而没有开启快捷键的话,你必须以盲人方式进入目的页面,相当的蛋疼。

二、属性解读

  • isAccessibilityElement 设置是否支持盲人模式 ,必须设置为YES系统才能聚焦到该元素。需要注意的是UIView是默认关闭的,UIButton、UILabel默认打开。
  • accessibilityLabel 用来描述控件是什么.UIButton和lable会默认从title和text中获取,textField从输入框的内容获取..当元素获得焦点时会第一个播放。
  • accessibilityTraits 元素的特征.如按钮,链接等.元素获取焦点后会在第二个播放.由于会播放按钮等,因此accessibilityLabel可以不用添加链接的描述,直接使用系统描述的特征,如:lable为登陆,traint为按钮,系统会播放”登陆->按钮”。
  • accessibilityHint 相当于是一个附加说明.第三个朗读,默认为nil。
  • accessibilityValue 元素的值.用在UISlider,UITextField等组件上.用来描述元素的值。
  • accessibilityFrame 元素的frame,当系统聚焦到当前元素时,会有一个黑色的外框,该值就是聚焦框的大小。当元素过小时可以通过设置该frame使得容易点击,这个不会改变app的UI.如果不想让系统读取到该元素,可以设置frame为CGRectZero。

三、特殊处理

  1. 如果是商品列表页,对应cell想要被聚焦后直接输出提示文案,只需要将自己设置为易用性元素,即self.isAccessibilityElement = YES;然后在能拿到业务数据的地方设置提示文案就好:self.accessibilityLabel = [NSString stringWithFormat:@"product name %@ %@",eventProduct.name,self.priceLbl.attributedText.string];
  2. 有的视图如果是自己绘制的话,像我们App里面购物车底部的支付按钮,这是用UIBezierPath画出来的。这种处理就会稍微麻烦一点。需要实现一个定义在UIAccessibilityContainer.h  文件中的非正式协议,不需要用尖括号遵循协议,只用实现以下几个方法就行。虽然系统聚焦框跟按钮实际形状有点偏差,但是影响不大。好在这种情况下,原来定义的点击事件也不需要特别处理。

    - (BOOL)isAccessibilityElement {    
        return NO;
    }     
    - (NSArray *)accessibilityElements {    
        return self.accessArray;
    }
    - (NSInteger)accessibilityElementCount {    
        return self.accessArray.count;
    }     
    
    @property (nonatomic, strong) NSArray           *accessArray;     
    - (NSArray *)accessArray {    
        if (_accessArray) {        
            return _accessArray;    
        }    
        UIAccessibilityElement *ele1 = [[UIAccessibilityElement alloc] initWithAccessibilityContainer:self.bgView];    
        CGRect rect1 = CGRectMake(CGRectGetMinX(self.bgView.frame), CGRectGetMinY(self.bgView.frame), CGRectGetWidth(self.bgView.frame)/2.0, CGRectGetHeight(self.bgView.frame)); 
        ele1.accessibilityFrame = UIAccessibilityConvertFrameToScreenCoordinates(rect1, self.bgView);    
        ele1.accessibilityLabel = @"PayPal";
        //这里最好加上这句,因为盲人用户听到这是按钮,才知道它可点击
        ele1.accessibilityTraits = UIAccessibilityTraitButton;        
    
        UIAccessibilityElement *ele2 = [[UIAccessibilityElement alloc] initWithAccessibilityContainer:self];    
        CGRect rect2 = CGRectMake(CGRectGetMidX(self.bgView.frame), CGRectGetMinY(self.bgView.frame), CGRectGetWidth(self.bgView.frame)/2.0, CGRectGetHeight(self.bgView.frame));  
        ele2.accessibilityFrame = UIAccessibilityConvertFrameToScreenCoordinates(rect2, self.bgView);    
        ele2.accessibilityLabel = PPString(SHOPPINGCART_PROCEEDTOCHECKOUT); 
        ele2.accessibilityTraits = UIAccessibilityTraitButton;       _accessArray = @[ele1, ele2];    
        return _accessArray;
    }



  3. 流程打断后的聚焦变化。当进入一个新界面后,系统默认会聚焦到左上角第一个元素,一般都是关闭按钮或者退出按钮。我们业务流程规定,在用户结账前,必须登录才能执行后续流程。所以如果一个盲人游客状态下加购了商品到购物车,此时点击结账。会先弹出登录注册弹窗让其登录。如果沿用系统的默认聚焦设定,将会非常不友好:因为用户先听到checkout button,点击后居然又直接听到了close button,下意识肯定会有点懵逼。所以这里我们最好手动调整聚焦。处理方式:我直接将账号输入栏的textField的accessibilityLabel设置为了@"Welcome, sign in or register to continue shopping.",然后在登录注册控制器的viewDidAppear中调用方法UIAccessibilityPostNotification(UIAccessibilityScreenChangedNotification, self.signInDataSource.emailTextField); 将界面聚焦到了账号输入栏。

  4. 在一些特殊流程阶段,用户返回后数据会丢失,所以在其点击返回按钮时,会给一个确认弹窗,当前项目中很多确认弹窗都是我们自己仿UIAlertView样式写的,然后添加在根window。所以这里也需要特别处理一下,否则弹窗出现以后靠左右扫的手势很难才能切换到我们的自定义弹窗上。其实核心方法跟上面一样,自定义弹窗添加到根window以后调用UIAccessibilityPostNotification(UIAccessibilityScreenChangedNotification, alertVew.titleLbl);方法将焦点聚焦到弹窗标题上。


四、写在最后

这是我在盲人模式适配中的一点小经验,做之前在网上搜的资料比较少也比较老,所以想着碰到的问题记录一下,如果能恰巧帮助到碰到这个问题的朋友那就更好了。突然要适配盲人模式的起因其实是因为我们的App和网站在美国被别人告了,说是不支持盲人使用,所以也只是大概适配了一下购物下单主流程,没有做得特别细。如果涉及到更细节深度的问题可能需要自己仔细阅读UIKit下的UIAccessibility.h文件了。


五、补充

流程切换导致的屏幕焦点切换,且需要加提示语音的问题。出现在我们App的详情页,具体情景就是在详情页点击加购按钮的时候,会弹起一个半弹窗,让用户选择颜色、尺寸、数量等。


点击确认会加购成功,收起半弹窗并将焦点设置在详情页的加购按钮;同时产品要求出一个“add to cart successfully”的语音反馈。


那么现在的问题就是怎么样在屏幕焦点从半弹窗的加购按钮切换为详情页加购按钮的情况下(此时系统在读的提示为“Add to Cart ,Button”),加入这个“add to cart successfully”的语音反馈。一开始我找到的是UIAccessibilityConstants.h文件下的UIAccessibilityElementFocusedNotification ,相当于在屏幕焦点切换时会发出通知。在它的下面还有两个东西:

// The corresponding value is the element that is now focused by the assistive technology.
UIKIT_EXTERN NSString *const UIAccessibilityFocusedElementKey NS_AVAILABLE_IOS(9_0);

// The corresponding value is the element that had previously been focused by the assistive technology.
UIKIT_EXTERN NSString *const UIAccessibilityUnfocusedElementKey NS_AVAILABLE_IOS(9_0);

看注释很容易想到他们三个是合在一起用的。所以我的解决方案就是详情页注册了UIAccessibilityElementFocusedNotification通知,在接收到通知的时候,用这两个key取到焦点切换前后的元素,如果符合条件的话,就延迟发送一个语音提示。主要代码如下:

//盲人模式下屏幕焦点切换通知
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(postAddToCartSuccessedVoiceTips:) name:UIAccessibilityElementFocusedNotification object:nil];

- (void)postAddToCartSuccessedVoiceTips:(NSNotification *)noti {
    UIButton *previous = noti.userInfo[UIAccessibilityUnfocusedElementKey];
    UIButton *current = noti.userInfo[UIAccessibilityFocusedElementKey];
    BOOL previousIsAddToCart = ISCLASS([UIButton class], previous) && ([previous.currentTitle isEqualToString:PPString(OK_STRING)] || [previous.currentTitle isEqualToString:PPString(PRODUCTDETAIL_ADDTOCART)]);
    BOOL currentIsAddToCart = ISCLASS([UIButton class], current) && [current.currentTitle isEqualToString:PPString(PRODUCTDETAIL_ADDTOCART)];
    if (previousIsAddToCart && currentIsAddToCart) {
        //判断屏幕焦点是从半弹窗的加购按钮 切换到的 详情页Add To cart按钮 ,需要提示加购成功语音
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            UIAccessibilityPostNotification(UIAccessibilityAnnouncementNotification, kAddToCartSuccessedTips);
        });
    }
}

但是这种粗糙的方式会造成一个问题,如果在2.5秒的时间内系统没有读完“Add to Cart ,Button”这句话的时候,你后续发送的语音提示会打断正在朗读的提示。或者如果“Add to Cart ,Button”朗读速度比较快的话,要等零点几秒才能听到加购成功的提示。所以这是一种不够优雅的解决方式。如果有更好的解决方案,还请不吝赐教!