Unable to configure voice over accessibility for a custom UITableViewCell

2016-10-21 ios objective-c uitableview voiceover uiaccessibility

Our iPhone app currently supports IOS 8/9/10. I am having difficulty supporting voice over accessibility for a custom UITableViewCell. I have gone through the following SO posts, but none of the suggestions have worked. I want individual components to be accessible.

  1. Custom UITableview cell accessibility not working correctly
  2. Custom UITableViewCell trouble with UIAccessibility elements
  3. Accessibility in custom drawn UITableViewCell
  4. https://developer.apple.com/library/content/documentation/UserExperience/Conceptual/iPhoneAccessibility/Making_Application_Accessible/Making_Application_Accessible.html#//apple_ref/doc/uid/TP40008785-CH102-SW10
  5. http://useyourloaf.com/blog/voiceover-accessibility/

Unfortunately for me, the cell is not detected by the accessibility inspector. Is there a way to voice over accessibility to pick up individual elements within the table view cell? When debugging this issue on both device and a simulator, I found that the XCode calls isAccessibleElement function. When the function returns NO, then the rest of the methods are skipped. I am testing on IOS 9.3 in XCode.

My custom table view cell consists of a label and a switch as shown below.

Custom UITableView Cell with a label and a switch

The label is added to the content view, while the switch is added to a custom accessory view.

The interface definition is given below

@interface MyCustomTableViewCell : UITableViewCell

///Designated initializer
- (instancetype)initWithReuseIdentifier:(NSString *)reuseIdentifier;


///Property that determines if the switch displayed in the cell is ON or OFF.
@property (nonatomic, assign) BOOL switchIsOn;

///The label to be displayed for the alert
@property (nonatomic, strong) UILabel *alertLabel;

@property (nonatomic, strong) UISwitch *switch;

#pragma mark - Accessibility
// Used for setting up accessibility values. This is used to generate accessibility labels of
// individual elements.
@property (nonatomic, strong) NSString* accessibilityPrefix;

-(void)setAlertHTMLText:(NSString*)title;


@end

The implementation block is given below

@interface MyCustomTableViewCell()

    @property (nonatomic, strong) UIView *customAccessoryView;
    @property (nonatomic, strong) NSString *alertTextString;
    @property (nonatomic, strong) NSMutableArray* accessibleElements;
@end

@implementation MyCustomTableViewCell


    - (instancetype)initWithReuseIdentifier:(NSString *)reuseIdentifier
    {
       if(self = [super initWithStyle:UITableViewCellStyleDefault        
             reuseIdentifier:reuseIdentifier]) {
           [self configureTableCell];
       }
       return self;
    }


    - (void)configureTableCell
    {
      if (!_accessibleElements) {
          _accessibleElements = [[NSMutableArray alloc] init];
      }


    //Alert label
    self.alertLabel = [[self class] makeAlertLabel];
    [self.contentView setIsAccessibilityElement:YES];
 // 
   [self.contentView addSubview:self.alertLabel];

   // Custom AccessoryView for easy styling.
  self.customAccessoryView = [[UIView alloc] initWithFrame:CGRectZero];
  [self.customAccessoryView setIsAccessibilityElement:YES];
  [self.contentView addSubview:self.customAccessoryView];

 //switch
  self.switch = [[BAUISwitch alloc] initWithFrame:CGRectZero];
  [self.switch addTarget:self action:@selector(switchWasFlipped:) forControlEvents:UIControlEventValueChanged];
 [self.switch setIsAccessibilityElement:YES];
 [self.switch setAccessibilityTraits:UIAccessibilityTraitButton];
 [self.switch setAccessibilityLabel:@""];
 [self.switch setAccessibilityHint:@""];
 self.switch.autoresizingMask = UIViewAutoresizingFlexibleTopMargin | UIViewAutoresizingFlexibleBottomMargin | UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleRightMargin;
 [self.customAccessoryView addSubview:self.switch];
}

+ (UILabel *)makeAlertLabel
{
  UILabel *alertLabel = [[UILabel alloc] initWithFrame:CGRectZero];
  alertLabel.backgroundColor = [UIColor clearColor];
  alertLabel.HTMLText = @"";
  alertLabel.numberOfLines = 0;
  alertLabel.lineBreakMode = LINE_BREAK_WORD_WRAP
  [alertLabel setIsAccessibilityElement:YES];
  return alertLabel;
}

-(void)setAlertHTMLText:(NSString*)title{ 
    _alertTextString = [NSString stringWithString:title];
    [self.alertLabel setText:_alertTextString];
}


- (BOOL)isAccessibilityElement {
    return NO;
}

  // The view encapsulates the following elements for the purposes of    
 // accessibility.
-(NSArray*) accessibleElements {
  if (_accessibleElements && [_accessibleElements count] > 0) {
      [_accessibleElements removeAllObjects];
  }
  // Fetch a new copy as the values may have changed.
  _accessibleElements = [[NSMutableArray alloc] init];
  UIAccessibilityElement* alertLabelElement =
  [[UIAccessibilityElement alloc] initWithAccessibilityContainer:self];
  //alertLabelElement.accessibilityFrame = [self convertRect:self.contentView.frame toView:nil];
  alertLabelElement.accessibilityLabel = _alertTextString;
  alertLabelElement.accessibilityTraits = UIAccessibilityTraitStaticText;
  [_accessibleElements addObject:alertLabelElement];


    UIAccessibilityElement* switchElement =
        [[UIAccessibilityElement alloc] initWithAccessibilityContainer:self];
  //  switchElement.accessibilityFrame = [self convertRect:self.customAccessoryView.frame toView:nil];
    switchElement.accessibilityTraits = UIAccessibilityTraitButton;
    // If you want custom values, just override it in the invoking function.
    NSMutableString* accessibilityString =
        [NSMutableString stringWithString:self.accessibilityPrefix];
    [accessibilityString appendString:@" Switch "];
    if (self.switchh.isOn) {
        [accessibilityString appendString:@"On"];
    } else {
        [accessibilityString appendString:@"Off"];
    }
    switchElement.accessibilityLabel = [accessibilityString copy];

    [_accessibleElements addObject:switchElement];
  }
  return _accessibleElements;
}

// In case accessibleElements is not initialized.
- (void) initializeAccessibleElements {
   _accessibleElements = [self accessibleElements];
}

- (NSInteger)accessibilityElementCount
{
    return [_accessibleElements count]
}

- (id)accessibilityElementAtIndex:(NSInteger)index
  {
    [self initializeAccessibleElements];
    return [_accessibleElements objectAtIndex:index];
 }

 - (NSInteger)indexOfAccessibilityElement:(id)element
 {
    [self initializeAccessibleElements];
    return [_accessibleElements indexOfObject:element];
}
@end

Answers

First of all, from the pattern you described, I'm not sure why you would want to differentiate between different elements in a cell. Generally, Apple keeps every cell a single accessibility element. A great place to see the expected iOS VO behavior for cells with labels and switches is in Settings App.

If you still believe the best way to handle your cells is to make them contain individual elements, then that is actually the default behavior of a cell when the UITableViewCell itself does not have an accessibility label. So, I've modified your code below and run it on my iOS device (running 9.3) and it works as you described you would like.

You'll notice a few things.

  1. I deleted all the custom accessibilityElements code. It is not necessary.
  2. I deleted the override of isAccessibilityElement on the UITableViewCell subclass itself. We want default behavior.
  3. I commented out setting the content view as an accessibilityElement -- we want that to be NO so that the tree-builder looks inside of it for elements.
  4. I set customAccessoryView's isAccessibilityElement to NO as well for the same reason as above. Generally, NO says "keep looking down the tree" and YES says "stop here, this is my leaf as far as accessibility is concerned."

I hope this is helpful. Once again, I do really encourage you to mimic Apple's VO patterns when designing for Accessibility. I think it's awesome that you're making sure your app is accessible!

#import "MyCustomTableViewCell.h"

@interface MyCustomTableViewCell()

@property (nonatomic, strong) UIView *customAccessoryView;
@property (nonatomic, strong) NSString *alertTextString;
@property (nonatomic, strong) NSMutableArray* accessibleElements;
@end

@implementation MyCustomTableViewCell

- (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier
{
    if(self = [super initWithStyle:UITableViewCellStyleDefault
                   reuseIdentifier:reuseIdentifier]) {
        [self configureTableCell];
    }
    return self;
}

// just added this here to get the cell to lay out for myself
- (void)layoutSubviews {
    [super layoutSubviews];

    const CGFloat margin = 8;

    CGRect b = self.bounds;

    CGSize labelSize = [self.alertLabel sizeThatFits:b.size];
    CGFloat maxX = CGRectGetMaxX(b);
    self.alertLabel.frame = CGRectMake(margin, margin, labelSize.width, labelSize.height);

    CGSize switchSize = [self.mySwitch sizeThatFits:b.size];
    self.customAccessoryView.frame = CGRectMake(maxX - switchSize.width - margin * 2, b.origin.y + margin, switchSize.width + margin * 2, switchSize.height);
    self.mySwitch.frame = CGRectMake(margin, 0, switchSize.width, switchSize.height);
}


- (void)configureTableCell
{
    //Alert label
    self.alertLabel = [[self class] makeAlertLabel];
    //[self.contentView setIsAccessibilityElement:YES];
    //
    [self.contentView addSubview:self.alertLabel];

    // Custom AccessoryView for easy styling.
    self.customAccessoryView = [[UIView alloc] initWithFrame:CGRectZero];
    [self.customAccessoryView setIsAccessibilityElement:NO]; // Setting this to NO tells the the hierarchy builder to look inside
    [self.contentView addSubview:self.customAccessoryView];
    self.customAccessoryView.backgroundColor = [UIColor purpleColor];

    //switch
    self.mySwitch = [[UISwitch alloc] initWithFrame:CGRectZero];
    //[self.mySwitch addTarget:self action:@selector(switchWasFlipped:) forControlEvents:UIControlEventValueChanged];
    [self.mySwitch setIsAccessibilityElement:YES]; // This is default behavior
    [self.mySwitch setAccessibilityTraits:UIAccessibilityTraitButton]; // No tsure why this is here
    [self.mySwitch setAccessibilityLabel:@"my swich"];
    [self.mySwitch setAccessibilityHint:@"Tap to do something."];
    self.mySwitch.autoresizingMask = UIViewAutoresizingFlexibleTopMargin | UIViewAutoresizingFlexibleBottomMargin | UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleRightMargin;
    [self.customAccessoryView addSubview:self.mySwitch];
}

+ (UILabel *)makeAlertLabel
{
    UILabel *alertLabel = [[UILabel alloc] initWithFrame:CGRectZero];
    alertLabel.backgroundColor = [UIColor clearColor];
    alertLabel.text = @"";
    alertLabel.numberOfLines = 0;
    [alertLabel setIsAccessibilityElement:YES];
    return alertLabel;
}

-(void)setAlertHTMLText:(NSString*)title{
    _alertTextString = [NSString stringWithString:title];
    [self.alertLabel setText:_alertTextString];
}

@end

Related