Change order of read items with VoiceOver

I have a bunch of buttons on the screen which are positioned intuitively visually but are not read in an intuitive order by VoiceOver. This is because certain buttons like Up and Down are placed above and below each other. However, voiceover starts reading from Left to Right, from Top to Bottom, it seems.

This results in voiceover reading the button to the right of "Up" after "Up", instead of reading "Down" immediately afterward.

How do I force voiceover to read the button that I want to read? I should mention that I'm using the swipe-to-cycle-through-elements feature on voiceover.

All my buttons are subclassed versions of UIView and UIButton. Here's an example of a button initiator I use. Ignore the pixel count - I know that's bad form but I'm in a pinch at the moment:

UIButton* createSpecialButton(CGRect frame, 
                                 NSString* imageName, 
                                 NSString* activeImageName,
                                 id target,
                                 SEL obClickHandler) 
{
    UIButton* b = [UIButton buttonWithType:UIButtonTypeCustom];
    [b setImage:[GlobalHelper nonCachedImage:imageName ofType:@"png"] 
       forState:UIControlStateNormal];
    [b setImage:[GlobalHelper nonCachedImage:activeImageName ofType:@"png"] 
       forState:UIControlStateHighlighted];
    [b addTarget:target action:obClickHandler forControlEvents:UIControlEventTouchUpInside];    
    b.frame= frame;
    return b;
}


- (UIButton *) createSendButton {
    CGFloat yMarker = 295;

    UIButton* b = createSpecialButton(CGRectMake(160, yMarker, 70, 45),
                                          @"Share_Btn",
                                          @"Share_Selected_Btn",
                                          self,
                                          @selector(sendAction));
    b.accessibilityHint = @"Send it!";
    b.accessibilityLabel = @"Stuff for voiceover to be added";
    [self.view addSubview:b];

    return b;
}

Answers


The easiest answer to this lies in creating a UIView subclass that contains your buttons, and responds differently to the accessibility calls from the system. These important calls are:

-(NSInteger)accessibilityElementCount
-(id)accessibilityElementAtIndex:
-(NSInteger)indexOfAccessibilityElement:

I've seen a few of these questions, and answered one before, but I've not seen a generic example of how to reorder the VoiceOver focus. So here is an example of how to create a UIView subclass that exposes its accessible subviews to VoiceOver by tag.

AccessibilitySubviewsOrderedByTag.h

#import <UIKit/UIKit.h>
@interface AccessibilitySubviewsOrderedByTag : UIView
@end

AccessibilitySubviewsOrderedByTag.m

#import "AccessibilityDirectional.h"
@implementation AccessibilitySubviewsOrderedByTag {
    NSMutableArray *_accessibilityElements;
}
    //Lazy loading accessor, avoids instantiating in initWithCoder, initWithFrame, or init.
-(NSMutableArray *)accessibilityElements{
    if (!_accessibilityElements){
        _accessibilityElements = [[NSMutableArray alloc] init];
    }
    return _accessibilityElements;
}
// Required accessibility methods...
-(BOOL)isAccessibilityElement{
    return NO;
}
-(NSInteger)accessibilityElementCount{
    return [self accessibilityElements].count;
}
-(id)accessibilityElementAtIndex:(NSInteger)index{
    return [[self accessibilityElements] objectAtIndex:index];
}
-(NSInteger)indexOfAccessibilityElement:(id)element{
    return [[self accessibilityElements] indexOfObject:element];
}
// Handle added and removed subviews...
-(void)didAddSubview:(UIView *)subview{
    [super didAddSubview:subview];
    if ([subview isAccessibilityElement]){
        // if the new subview is an accessibility element add it to the array and then sort the array.
        NSMutableArray *accessibilityElements = [self accessibilityElements];
        [accessibilityElements addObject:subview];
        [accessibilityElements sortUsingComparator:^NSComparisonResult(id obj1, id obj2){
            // Here we'll sort using the tag, but really any sort is possible.
            NSInteger one = [(UIView *)obj1 tag];
            NSInteger two = [(UIView *)obj2 tag];
            if (one < two) return NSOrderedAscending;
            if (one > two) return NSOrderedDescending;
            return NSOrderedSame;
        }];
    }
}
-(void)willRemoveSubview:(UIView *)subview{
    [super willRemoveSubview:subview];
    // Clean up the array. No check since removeObject: is a safe call.
    [[self accessibilityElements] removeObject:subview];
}
@end

Now simply enclose your buttons in an instance of this view, and set the tag property on your buttons to be essentially the focus order.


You can change the order setting the view's accessibilityElements array:

self.view.accessibilityElements = @[self.view1, self.view2, self.view3, self.view4];

or

self.anotherView.accessibilityElements = @[self.label1, self.txtView1, self.label2, self.txtView2];

If you need to set the interaction enabled programmatically:

[self.view1 setUserInteractionEnabled:YES];

If the view is hidden the voice over will not pass through it.


In Swift you just have to set view's accessiblityElements array property:

view.accessibilityElements = [view1, view2, view3] // order you wish to have


I tried Wesley's answer of setting the array of the accessibilityElements but it didn't work for me.

Apple has some documentation Enhancing the Accessibility of Table View Cells with an example in code. Basically you set the accessibility label of the cell (the parent view) to the values of the accessibility labels of the child views.

[cell setAccessibilityLabel:[NSString stringWithFormat:@"%@, %@", cityLabel, temperatureLabel]];

This is what worked for me.


This doesn’t directly answer the original question, but it answers the title of the question:

When I want VoiceOver to swipe down a column, I have been using a containing view for the column with shouldGroupAccessibilityChildren set.

I wish I had known this earlier, because it can be a pain to retroactively insert containers into an autolayout situation…


I know this is an old thread, but I found that the easiest way to do it is to subclass UIView as such (Swift 3). Then simply modify your main UIView type in storyboard to AccessibiltySubviewsOrderedByTag and update the tags in each subview you want read in order.

class AccessibilitySubviewsOrderedByTag: UIView {

override func layoutSubviews() {

    self.accessibilityElements = [UIView]()
    for accessibilitySubview in self.subviews {
        if accessibilitySubview.isAccessibilityElement {
            self.accessibilityElements?.append(accessibilitySubview)
        }
    }
    self.accessibilityElements?.sort(by: {($0 as AnyObject).tag < ($1 as AnyObject).tag})
}

}


I think you can do it in the storyboard. The VoiceOver order is determined by the order of the views in the document outline.

Just drag and drop the views in the view hierarchy in the right order.

Edit:

Sorry I can not post screenhots until 10 reputation. In the storyboard, the document outline is the area on the left where your scenes with their subviews are listed. Here, subviews are ordered one below each other. When you change this order, the reading-order of VoiceOver will change.


I found a convenience way yesterday. Similar to @TejAces ' answer. Make a new swift file, then copy these things into it.

import UIKit

extension UIView {
    func updateOrder(_ direction: Bool = true) {
        var tempElements: [Any]? = [Any]()
        let views = (direction) ? subviews : subviews.reversed()
        for aView in views {
            tempElements?.append(aView)
        }
        accessibilityElements = tempElements
    }
}

class ReorderAccessibilityByStoryBoardView: UIView {
    override func didAddSubview(_ subview: UIView) {
        updateOrder()
    }
}

Set the UIView(contains views you want to reorder)'s class as ReorderAccessibilityByStoryBoardView. Then you can reorder them by reordering storyboard's view list.

Because subview doesn't contain views in StackView/ScrollView, you need to make a independent class in this file. Such as the ReorderAccessibilityByStoryBoardStackView down below.

class ReorderAccessibilityByStoryBoardStackView: UIStackView {
    override func didAddSubview(_ subview: UIView) {
        updateOrder(false)
    }
}

With these codes, you can also reorder view's added in code by adding them in a specific order.


Need Your Help

DOM, use a variable font-size

javascript css dom

I'm trying to manipulate font-size with a variable, but it is not working. Can anybody see what is wrong?

Error printing string formatted with %.0f using an int substitute

java string-formatting

I am trying to print the string in toString(). It prints fine if I remove the %.0f and the this.years substitute, but does not print if I include it. I am not sure why. The only reason I can thi...