Change order of read items with VoiceOver

42,654

Solution 1

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.

Solution 2

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.

Solution 3

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

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

Solution 4

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})
    }
}

Solution 5

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…

Share:
42,654
Mark S
Author by

Mark S

Updated on July 09, 2022

Comments

  • Mark S
    Mark S almost 2 years

    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;
    }
    
  • MusiGenesis
    MusiGenesis about 10 years
    I wonder if this still works in iOS 7? I've put in all these methods, and they're just never called by iOS no matter what I do, so they have no effect on anything.
  • NJones
    NJones about 10 years
    @MusiGenesis I just tested the project that this code is from on an iPad mini Retina running 7.1 from Xcode 5.1.1 and it ran as expected. These methods won't be called unless VoiceOver is on. Also, they may be obstructed by views closer to the root view. Can't really help much without more information, except to say that they do still work.
  • Dynamite
    Dynamite over 9 years
    Is there any protocol that specifies which all methods need to be implemented ? Can you specify any links to apple docs that might be helpful ?
  • nycynik
    nycynik about 9 years
    Can you add a screen shot? I can't see where you mean, thanks.
  • Ken Ko
    Ken Ko almost 9 years
    Apple docs say that "order of accessibility elements within the container view should be the same as the order in which the represented elements are presented to the user, from top-left to bottom-right." From my own testing, voice over by default will do top-left to bottom-right, regardless of the order of the views.
  • lifemoveson
    lifemoveson over 8 years
    this does not work for my application...I did try to make sure all the views are not hidden.
  • Daniel T.
    Daniel T. over 7 years
    Don't forget to set the parent view's isAccessibilityElement property to true.
  • David Dunham
    David Dunham about 7 years
    I’ve only seen the left-to-right, then top-to-bottom, order myself.
  • David Dunham
    David Dunham almost 7 years
    Oh, nifty approach using tags. Alas, I often use tags for other purposes (such as doing radio button groups), but this might help.
  • David Dunham
    David Dunham almost 7 years
    Swift is just a programming language. This technique works in Objective-C as well, but it can be a pain when you don’t have outlets for every view.
  • Zoltán
    Zoltán over 6 years
    I like this answer, it's much more simple than subclassing UIView from the superview.
  • nacho4d
    nacho4d almost 6 years
    In objective-c, in this case, just add an @ before the literal array view.accessibilityElements = @[view1, view2, view3]
  • HASSAN MD TAREQ
    HASSAN MD TAREQ about 4 years
    "The VoiceOver order is determined by the order of the views in the document outline" -> Wrong ( @KenKo's comment is correct )