iOS developer guide

This guide aims to present the various iOS SDK accessibility options. Through different categories, this guide explains how to use the accessibility attributes / methods and provides links to the official documentation from Apple. Code snippets are also available to show you how to implement it (Swift 4, Objective C).

Text alternatives

Description

On iOS, the vocalization of an element is done through four attributes: label, hint, value and trait. The order of vocalization is always as follows: label, value, trait and hint. This order cannot be changed and the vocalization is performed only once.

A section of this guide is dedicated to the trait, we describe here the other three:

  • accessibilityLabel : the label redefines the text read by VoiceOver. This allows a component to be more explicit than the text displayed on the screen. For example, for a button whose title is “OK”, this attribute can indicate that the button is used to confirm an action.
  • accessibilityValue : the value of an element is by default the completion percentage (e.g. a progress bar percentage). Note that for most elements available in the SDK, this value does not need to be set (the system automatically sets the value).
  • accessibilityHint : the hint describes the component’s behaviour. Example: “click here to get the result”.

These accessibility attributes are available via the builder interface but also programmatically. Anything inheriting from UIView has these attributes by default. These attributes accept an optional string, and are therefore easily localizable.

Example


@interface ChangeTextView() {

    __weak IBOutlet UILabel * myLabel;
    __weak IBOutlet UIProgressView * myProgressView;
}
@end

@implementation ChangeTextView

- (void)viewDidAppear:(BOOL)animated {
    [super viewDidAppear:animated];

    myLabel.accessibilityLabel = @"hello";
    myLabel.accessibilityHint = @"This is an added comment.";

    myProgressView.accessibilityValue = @"45 per cent";
}
@end

class ChangeTextView: UIViewController {

    @IBOutlet weak var myLabel: UILabel!
    @IBOutlet weak var myProgressView: UIProgressView!

    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)

        myLabel.accessibilityLabel = "hello"
        myLabel.accessibilityHint = "This is an added comment."

        myProgressView.accessibilityValue = "45 per cent"

    }
}

Element trait

Description

The accessibilityTraits attribute allows to specify the trait of an element to the accessibility API. Thus, it is possible to make a list item be considered as a button because it is clickable. Therefore, the accessibilityTrait attribute plays an important role on the element vocalization because the trait is vocalized by VoiceOver.

This accessibility attribute is available via the builder interface but also programmatically.

There are many available traits. The more commonly used are:

  • accessibilityTraitNone : removes any semantic value to the element.
  • accessibilityTraitButton : adds the “button” trait, the element is seen as a button by VoiceOver.
  • accessibilityTraitLink : useful to define a label as a “link”.
  • accessibilityTraitHeader : defines an element as a header (see the « Title and header » section).
  • accessibilityTraitAdjustable : defines an element as an “adjustable” element, that is to say an element that users can adjust in a continuous manner, such as a slider or a picker view (see VoiceOver gestures documentation).

Example


- (void)customTraits() {
    //Specified UIPageControl with the ’ajustable’ trait.
    pageControl.accessibilityTraits = UIAccessibilityTraitAdjustable;

    //Added header.  
    defaultHeaderViewCell.accessibilityTraits = UIAccessibilityTraitHeader;

    //Many traits possible combination.  
    onePageButton.accessibilityTraits = UIAccessibilityTraitButton + UIAccessibilityTraitSelected;
}

func customTraits() {
    //Specified UIPageControl with the ’ajustable’ trait.
    pageControl.accessibilityTraits = UIAccessibilityTraitAdjustable

    //Added header.  
    defaultHeaderViewCell.accessibilityTraits = UIAccessibilityTraitHeader

    //Many traits possible combination.  
    onePageButton.accessibilityTraits = UIAccessibilityTraitButton + UIAccessibilityTraitSelected
}

Hide elements from accessibility

Description

It is possible via an accessibility attribute to hide elements from accessibility tools (e.g. VoiceOver). By extension, it is possible to force some elements to be visible to accessibility tools.

  • isAccessibilityElement : boolean to specify that an element is visible or not to the Accessibility API (VoiceOver or other).
  • accessibilityElementsHidden : boolean to indicate that the children elements of the target element are visible or not to the Accessibility API.
  • accessibilityViewIsModal : boolean that can make visible or not the sibling elements of the target element to the Accessibility API.
    A theoretical explanation and a practical example are provided in a video detailed in the WWDC part.

The accessibilityElement attribute is available via the interface builder but can also be used directly through the code. The other two attributes are available only through the code.

Example

A red square will be drawn and contain two other squares (blue and yellow) in order to apply the attributes defined hereabove.


- (void)viewDidAppear:(BOOL)animated {
    [super viewDidAppear:animated];

    //Creation of an element inside which 2 other children elements will be inserted.
    CGRect parentViewRect = CGRectMake(100.0, 100.0, 40.0, 40.0);
    UIView * myParentView = [[UIView alloc]initWithFrame:parentViewRect];
    myParentView.backgroundColor = [UIColor redColor];

    [UIApplication.sharedApplication.keyWindow addSubview:myParentView];

    //The target element musn't be accessible so as to be considered as a container to its children elements.
    //If this attribute is 'YES', the target element will be the only one accessible element.
    myParentView.isAccessibilityElement = NO;

    //The elements contained in the target element won't be accessible even if they're defined as such.
    //If this attribute is 'NO' and the previous one is 'NO', only the children elements will be accessible.
    myParentView.accessibilityElementsHidden = NO;

    [self createViewWithColor:[UIColor yellowColor] 
                       inside:myParentView];
    [self createViewWithColor:[UIColor blueColor] 
                       inside:myParentView];
}

- (void)createViewWithColor:(UIColor*)color
                     inside:(UIView*)parentView {

    float delta = (color == [UIColor yellowColor]) ? 0.0 : 20.0;

    CGRect rect = CGRectMake(10.0 + delta, 10.0 + delta, 10.0, 10.0);
    UIView * theView = [[UIView alloc]initWithFrame:rect];
    theView.backgroundColor = color;

    [parentView addSubview:theView];

    theView.isAccessibilityElement = YES;
}

override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)

        //Creation of an element inside which 2 other children elements will be inserted.
        let parentViewRect = CGRect.init(x: 100.0,
                                         y: 100.0,
                                         width: 40.0,
                                         height: 40.0)
        let myParentView = UIView.init(frame: parentViewRect)
        myParentView.backgroundColor = .red

        UIApplication.shared.keyWindow?.addSubview(myParentView)

        //The target element musn't be accessible so as to be considered as a container to its children elements.
        //If this attribute is 'true', the target element will be the only one accessible element.
        myParentView.isAccessibilityElement = true

        //The elements contained in the target element won't be accessible even if they're defined as such.
        //If this attribute is 'false' and the previous one is 'false', only the children elements will be accessible.
        myParentView.accessibilityElementsHidden = false

        self.createViewWithColor(.yellow, inside: myParentView)
        self.createViewWithColor(.blue, inside: myParentView)
    }

    func createViewWithColor(_ color:UIColor, inside parentView:UIView) {

        let delta:CGFloat = ((color == .yellow) ? 0.0 : 20.0)
        let rect = CGRect.init(x: 10.0 + delta,
                               y: 10.0 + delta,
                               width: 10.0,
                               height: 10.0)

        let theView = UIView.init(frame: rect)
        theView.backgroundColor = color

        parentView.addSubview(theView)

        theView.isAccessibilityElement = true
    }

Trigger a vocalization

Description

It is very easy to trigger vocalizations with VoiceOver.
Note that we are talking about VoiceOver vocalization and not TTS (Text To Speech) that can operate whether VoiceOver is on or off.

To trigger a vocalization, just call the UIAccessibilityPostNotification method passing the notification allowing to trigger a vocalization (UIAccessibilityAnnouncementNotification) and the string to vocalize as parameters.

Note: the vocalization is done in the system’s language.

Example


UIAccessibilityPostNotification(UIAccessibilityAnnouncementNotification, @"This is a VoiceOver message.");

UIAccessibilityPostNotification(UIAccessibilityAnnouncementNotification, "This is a VoiceOver message.")

Check accessibility options state

Description

On iOS, it is possible to check the accessibility options state. Is VoiceOver activated? Is the audio-mono mode activated? Several methods can help you to check with that. They are part of the UIKit framework.
The most useful method is UIAccessibilityIsVoiceOverRunning which allows to know whether VoiceOver is activated.

Many other methods are available in the links hereafter and some of them are deeply explained in a WWDC video.

Exemple


    BOOL isVoiveOverRunning = (UIAccessibilityIsVoiceOverRunning() ? 1 : 0);
    BOOL isSwitchControlRunning = (UIAccessibilityIsSwitchControlRunning() ? 1 : 0);

    NSLog(@"VoiceOver is %d and SwitchControl is %d.", isVoiveOverRunning, isSwitchControlRunning);

    let isVoiceOverRunning = (UIAccessibilityIsVoiceOverRunning() ? 1 : 0)
    let isSwitchControlRunning = (UIAccessibilityIsSwitchControlRunning() ? 1 : 0)

    print("VoiceOver is \(isVoiceOverRunning) and SwichControl is \(isSwitchControlRunning).")

Notify a content change

Description

When there is a content change in the current page, it is possible to notify the accessibility API using several types of notifications. To do that, we must send the change notification to the accessibility API using the following method: UIAccessibilityPostNotification.

There are several types of change notifications but the two most commonly used are:

  • UIAccessibilityLayoutChangedNotification : notifies that a part of the page has changed with 2 possible incoming parameters (a NSString or a UIObject).
    With a NSString, the notification behaves like a UIAccessibilityAnnouncementNotification with a VoiceOver vocalization.
    With a UIObject, focus is shifted to the user interface element.
    This notification is very similar to the UIAccessibilityAnnouncementNotification but should come as a result of dynamic content being deleted or added to the current view.
  • UIAccessibilityScreenChangedNotification : notifies that the whole page has changed including nil or a UIObject as incoming parameters.
    With nil, the first accessible element in the page is focused.
    With a UIObject, focus is shifted to the specified element with a VoiceOver.
    This notification comes along with a vocalization including a sound like announcing a new page.

Example


//The element'myLabel' is focused and vocalized with its new value.
- (IBAction)tapHere:(UIButton *)sender {

    myLabel.accessibilityLabel = @"This is a new label.";
    UIAccessibilityPostNotification(UIAccessibilityLayoutChangedNotification, myLabel);
}

//The first accessible element in the page is focused and vocalized with a sound like announcing a new page.
- (IBAction)clic:(UIButton *)sender {

    UIAccessibilityPostNotification(UIAccessibilityScreenChangedNotification, nil);
}

//The element'myLabel' is focused and vocalized with its new value.
@IBAction func tapHere(_ sender: UIButton) {

    myLabel.accessibilityLabel = "This is a new label."
    UIAccessibilityPostNotification(UIAccessibilityLayoutChangedNotification, myLabel)
}

//The first accessible element in the page is focused and vocalized with a sound like announcing a new page.
@IBAction func clic(_ sender: UIButton) {

    UIAccessibilityPostNotification(UIAccessibilityScreenChangedNotification, mySecondLabel)
}

Change the vocalization language

Description

To change the vocalization language of VoiceOver for a word or a sentence, one can use the accessibilityLanguage  attribute.
Available through the UIAccessibility informal protocol, this attribute allows to specify a language for a dedicated text.
For instance, if we use this attribute on a UILabel, it will be vocalized by VoiceOver in the language set on this attribute.

Example


- (IBAction)tapHere:(UIButton *)sender {

    myLabel.accessibilityLanguage = @"fr";
    myLabel.accessibilityLabel = @"Ceci est un nouveau label. Merci.";
    UIAccessibilityPostNotification(UIAccessibilityLayoutChangedNotification, myLabel);
}

@IBAction func tapHere(_ sender: UIButton) {

    myLabel.accessibilityLanguage = "fr"
    myLabel.accessibilityLabel = "Ceci est un nouveau label. Merci."
    UIAccessibilityPostNotification(UIAccessibilityLayoutChangedNotification, myLabel)
}

Modify the focus area of VoiceOver

Description

In the case of dynamically modified element or component not inheriting from UIView, it is possible to modify the focus area of accessibility of this element, i.e. the area VoiceOver highlights when focusing an element.

  • accessibilityFrame : sets the area via a rectangle (CGRect).
    Usually, for an element inheriting from UIView, this area is the « visible » part of the view.
  • accessibilityPath : equivalent to AccessibilityFrame but sets the area via Bezier curves.
  • accessibilityActivationPoint : defines a contact point inside the frame whose action will be fired by a double-tap element activation.
    The default value is the midpoint of the frame but it can be redefine anywhere inside.
    A classical use case could be an easy activation inside a regroupment of elements for instance.

    By keeping this default value, one might unwillingly activate the element in the middle of the frame only by activating the created regroupment.

Example


float xVal;
float yVal;
float widthVal;
float heightVal;

- (void)viewDidAppear:(BOOL)animated {
    [super viewDidAppear:animated];

    xVal = myLabel.accessibilityFrame.origin.x;
    yVal = myLabel.accessibilityFrame.origin.y;
    widthVal = myLabel.accessibilityFrame.size.width;
    heightVal = myLabel.accessibilityFrame.size.height;

}

//First way to enlarge the focus area.
- (IBAction)tapHere:(UIButton *)sender {

    myLabel.accessibilityFrame = CGRectMake(xVal,
                                            yVal,
                                            widthVal + 100.0,
                                            heightVal+ 100.0);

    UIAccessibilityPostNotification(UIAccessibilityLayoutChangedNotification, myLabel);
}

//Second way to enlarge the focus area (Bezier).
- (IBAction)clic:(UIButton *)sender {

    UIBezierPath * bezierPath = [UIBezierPath bezierPath];

    [bezierPath moveToPoint:CGPointMake(xVal, yVal)];

    [bezierPath addLineToPoint:CGPointMake(xVal + widthVal + 100.0, 
                                           yVal)];
    [bezierPath addLineToPoint:CGPointMake(xVal + widthVal + 100.0, 
                                           yVal + heightVal+ 100.0)];
    [bezierPath addLineToPoint:CGPointMake(xVal, 
                                           yVal + heightVal+ 100.0)];
    [bezierPath closePath];

    myLabel.accessibilityPath = bezierPath;

    UIAccessibilityPostNotification(UIAccessibilityLayoutChangedNotification, myLabel);
}

    var xVal: CGFloat = 0.0
    var yVal: CGFloat = 0.0
    var widthVal: CGFloat = 0.0
    var heightVal: CGFloat = 0.0

    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)

        xVal = myLabel.accessibilityFrame.origin.x;
        yVal = myLabel.accessibilityFrame.origin.y;
        widthVal = myLabel.accessibilityFrame.size.width;
        heightVal = myLabel.accessibilityFrame.size.height;
    }

    //Première façon d'augmenter la zone de focus.
    @IBAction func clicHere(_ sender: UIButton) {

        myLabel.accessibilityFrame = CGRect.init(x: xVal,
                                                 y: yVal,
                                                 width: widthVal + 100.0,
                                                 height: heightVal + 100.0)

        UIAccessibilityPostNotification(UIAccessibilityLayoutChangedNotification, myLabel)
    }

    //Seconde façon d'augmenter la zone de focus (Bézier).
    @IBAction func clic(_ sender: UIButton) {

        let bezierPath = UIBezierPath.init()

        bezierPath.move(to: CGPoint.init(x: xVal, y: yVal))

        bezierPath.addLine(to: CGPoint.init(x: xVal + widthVal + 100.0,
                                            y: yVal))
        bezierPath.addLine(to: CGPoint.init(x: xVal + widthVal + 100.0,
                                            y: yVal + heightVal + 100.0))
        bezierPath.addLine(to: CGPoint.init(x: xVal,
                                            y: yVal + heightVal + 100.0))
        bezierPath.close()

        myLabel.accessibilityPath = bezierPath

        UIAccessibilityPostNotification(UIAccessibilityScreenChangedNotification, myLabel)
    }

Grouping elements

Description

Grouping elements may be used to vocalize the bundle once and to associate a dedicated action to it.

Example 1

We wish to obtain a 'label' and a 'switch control' as one unique block behaving like a switch control.
In this case, a view must be created to encapsulate all the elements and an action must be implemented (only the container must be an accesible element).

Create your wrapper as an accessible element :


#import "MyViewController.h"
#import "MyWrapView.h"

@interface MyViewController ()

@property (weak, nonatomic) IBOutlet UILabel * myLabel;
@property (weak, nonatomic) IBOutlet UISwitch * mySwitch;

@end


@implementation MyViewController

- (void)viewDidAppear:(BOOL)animated {
    [super viewDidAppear:animated];

    //Create the view that will encapsulate the 'label' and the 'Switch Control'.
    MyWrapView * wrap = [[MyWrapView alloc] initWith:_myLabel
                                                 and:_mySwitch];

    [self.view addSubview:wrap];
}
@end

    class MyViewController: UIViewController {

    @IBOutlet weak var myLabel: UILabel!
    @IBOutlet weak var mySwitch: UISwitch!


    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)

        //Create the view that will encapsulate the 'label' and the 'Switch Control'.
        let wrap = MyWrapView.init(with: myLabel,
                                   and: mySwitch)

        self.view.addSubview(wrap)
    }
}


... and implement the wrapper class to define accurately the action when a double tap occurs :


@implementation MyWrapView

//Indexes for the array containing all the wrapped elements.
int indexLabel = 0;
int indexSwitch = 1;


- (instancetype)initWith:(UILabel *)label and:(UISwitch *)aSwitch {

    CGRect viewFrame = CGRectUnion(label.frame, aSwitch.frame);
    MyWrapView * wrapView = [[MyWrapView alloc]initWithFrame:viewFrame];

    wrapView.accessibilityElements = @[label, aSwitch];

    NSString * switchValue = (aSwitch.isOn) ? @"on" : @"off";

    wrapView.isAccessibilityElement = YES;
    wrapView.accessibilityLabel = [NSString stringWithFormat:@" %@ %@",
                                   @"the switch control is",
                                   switchValue.description];
    wrapView.accessibilityHint = @"tap twice to change the switch control status.";

    return wrapView;
}


//Function called by the system when a double tap occurs on the selected wrapper.
- (BOOL)accessibilityActivate {

    UISwitch * theSwitch = self.accessibilityElements[indexSwitch];
    [theSwitch setOn:!(theSwitch.isOn)];

    NSString * switchValue = (theSwitch.isOn) ? @"on" : @"off";

    self.accessibilityLabel = [NSString stringWithFormat:@" %@ %@",
                               @"the switch control is",
                               switchValue.description];
    return YES;
}
@end

    class MyWrapView: UIView {

    //Indexes for the array containing all the wrapped elements.
    let indexLabel = 0
    let indexSwitch = 1


    override init(frame: CGRect) {
        super.init(frame: frame)
    }


    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
    }


    convenience init(with label: UILabel,and aSwitch: UISwitch) {

        let viewFrame = label.frame.union(aSwitch.frame)
        self.init(frame: viewFrame)

        self.accessibilityElements = [label, aSwitch]

        let switchValue = (aSwitch.isOn) ? "on" : "off"

        self.isAccessibilityElement = true
        self.accessibilityLabel = "the switch control is" + switchValue.description
        self.accessibilityHint = "tap twice to change the switch control status."
    }


    //Function called by the system when a double tap occurs on the selected wrapper.
    override func accessibilityActivate() -> Bool {

        let theSwitch = self.accessibilityElements?[indexSwitch] as? UISwitch
        theSwitch?.setOn(!((theSwitch?.isOn)!), animated: false)

        let switchValue = (theSwitch?.isOn)! ? "on" : "off"

        self.accessibilityLabel = "the switch control is" + switchValue.description

        return true
    }
}

Example 2

We have a button, a label and a switch control to be regrouped in a single block whose activation will change the switch control status automatically without defining any action like before.
The easiest way would be to place the switch control in the middle of the created frame in order to locate its accessibilityActivationPoint directly on it.
Unfortunately, that's not always possible.

A new accessible element must then be created to gather all the desired objects and its accessibilityActivationPoint has to be defined on the switch control.


@interface ActivationPointViewController ()

@property (weak, nonatomic) IBOutlet UIButton * myButton;
@property (weak, nonatomic) IBOutlet UILabel * myLabel;
@property (weak, nonatomic) IBOutlet UISwitch * mySwitch;

@end


@implementation ActivationPointViewController

- (void)viewDidAppear:(BOOL)animated {
    [super viewDidAppear:animated];

    UIAccessibilityElement * elt = [[UIAccessibilityElement alloc]initWithAccessibilityContainer:self.view];

    CGRect a11yFirstEltFrame = CGRectUnion(_myLabel.frame, _myButton.frame);
    CGRect a11yEltFrame = CGRectUnion(a11yFirstEltFrame, _mySwitch.frame);

    elt.accessibilityLabel = @"regrouping elements";
    elt.accessibilityHint = @"double tap to change the switch control status";
    elt.accessibilityFrameInContainerSpace = a11yEltFrame;
    elt.accessibilityActivationPoint = [_mySwitch center];

    self.view.accessibilityElements = @[elt];
}
@end

    class ActivationPointViewController: UIViewController {

    @IBOutlet weak var myButton: UIButton!
    @IBOutlet weak var myLabel: UILabel!
    @IBOutlet weak var mySwitch: UISwitch!


    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)

        let elt = UIAccessibilityElement(accessibilityContainer: self.view)
        let a11yEltFrame = (myLabel.frame.union(myButton.frame)).union(mySwitch.frame)

        elt.accessibilityLabel = "regrouping elements"
        elt.accessibilityHint = "double tap to change the switch control status"
        elt.accessibilityFrameInContainerSpace = a11yEltFrame
        elt.accessibilityActivationPoint = mySwitch.center

        self.view.accessibilityElements = [elt]
    }
}

Another grouping elements case could use the shouldGroupAccessibilityChildren attribute which is a Boolean that indicates whether VoiceOver must group its children views.
This allows making unique vocalizations or define a particular reading order for a part of the page (see Reading order section for further information).

Accessibility events

Description

iOS sends several accessibility events to the applications. They are sent when accessibility options are changed. For example, if VoiceOver is deactivated, the running applications will receive the UIAccessibilityVoiceOverStatusDidChange event. This is very useful when used simultaneously with UIAccessibilityIsVoiceOverRunning.

Let's say the application behaves differently when VoiceOver is turned on. This is detected by the UIAccessibilityIsVoiceOverRunning method. What happens if VoiceOver is disabled? This is when the system events can be used. By listening to these events, it is possible to dynamically change how the application behaves.

Example

In this example, a method is fired when VoiceOver or Switch Control status has changed.


- (void)viewDidAppear:(BOOL)animated {
    [super viewDidAppear:animated];

    [[NSNotificationCenter defaultCenter] addObserver:self
                                             selector:@selector(methodToBeCalled:)
                                                 name:UIAccessibilitySwitchControlStatusDidChangeNotification
                                               object:nil];

    [[NSNotificationCenter defaultCenter] addObserver:self
                                             selector:@selector(methodToBeCalled:)
                                                 name:UIAccessibilityVoiceOverStatusDidChangeNotification
                                               object:nil];
}

- (void)methodToBeCalled:(NSNotification *)notification {

    NSArray * checkStatus = @[@"NOK", @"OK"];

    NSLog(@"SWITCH CONTROL is %@ and VOICE OVER is %@",
          checkStatus[UIAccessibilityIsSwitchControlRunning()],
          checkStatus[UIAccessibilityIsVoiceOverRunning()]);
}

    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)

        NotificationCenter.default.addObserver(self,
                                               selector: #selector(methodToBeCalled(notification:)),
                                               name: .UIAccessibilitySwitchControlStatusDidChange,
                                               object: nil)

        NotificationCenter.default.addObserver(self,
                                               selector: #selector(methodToBeCalled(notification:)),
                                               name: .UIAccessibilityVoiceOverStatusDidChange,
                                               object: nil)
    }

    @objc private func methodToBeCalled(notification: Notification) {

        let switchControlStatus = (UIAccessibilityIsSwitchControlRunning() ? "OK" : "NOK")
        let voiceOverStatus = (UIAccessibilityIsVoiceOverRunning() ? "OK" : "NOK")

        print("SWITCH CONTROL is \(switchControlStatus) and VOICE OVER is \(voiceOverStatus).")
    }

All accessibility events are available on the official documentation from Apple.

Text size

Description

Since iOS7, it is possible to make the text size dynamic according to the phone settings.
larger accessibility sizes option screenshot
The following steps should be respected in order to easily use this API :

  • Choose the system font to facilitate your programing even if the use of other fonts is well assisted by the UIFontMetrics new class (iOS11).

    
     __weak IBOutlet UILabel * fontHeadline;
     __weak IBOutlet UILabel * fontFootNote;
    
     //Use of the default native font for a header.
     UIFont * myFont = [UIFont preferredFontForTextStyle:UIFontTextStyleHeadline];
    
     //Personal font definition for a header.
     UIFont * fontHead = [UIFont fontWithName:@"Chalkduster" size:30.0];
     UIFontMetrics * fontHeadMetrics = [[UIFontMetrics alloc]initForTextStyle:UIFontTextStyleHeadline];
     fontHeadline.font = [fontHeadMetrics scaledFontForFont:fontHead];
    
    
     @IBOutlet weak var fontHeadline: UILabel!
     @IBOutlet weak var fontFootNote: UILabel!
    
     //Use of the default native font for a header.
     let myFont = UIFont.preferredFont(forTextStyle: .headline)
    
     //Personal font definition for a header.
     let fontHead = UIFont(name: "Chalkduster", size: 30.0)
     let fontHeadMetrics = UIFontMetrics(forTextStyle: .headline)
     fontHeadline.font = fontHeadMetrics.scaledFont(for: fontHead!)
    
  • Listen to the font size settings change event UIContentSizeCategoryDidChange or directly use the property adjustsFontForContentSizeCategory to have an automatic update of your system font size if you're programming in iOS10 (this attribute applies to custom fonts only with the UIFontMetrics class).
    Note that the traitCollectionDidChange method that belongs to the UITraitEnvironment informal protocol may also be used in this context because it will be called as soon as the iOS interface environment changes (class/content size, portrait/landscape).

    
     //Listens to the notification dealing with the font size changing from the mobile settings.
     [[NSNotificationCenter defaultCenter] addObserver:self
                                              selector:@selector(methodToBeCalled:)
                                                  name:UIContentSizeCategoryDidChangeNotification
                                                object:nil];
    
     //Automatic changing of the font size without listening to the previous notification.
     fontHeadline.adjustsFontForContentSizeCategory = YES;
    
     - (void)methodToBeCalled:(NSNotification *)notification {
    
         //When handling the font size change event, you must redisplay the affected elements.
         fontFootNote.font = [UIFont preferredFontForTextStyle:UIFontTextStyleFootnote];
     }
    
    
     //Listens to the notification dealing with the font size changing from the mobile settings.
     NotificationCenter.default.addObserver(self,
                                            selector:#selector(methodToBeCalled(notification:)),
                                            name: .UIContentSizeCategoryDidChange,
                                            object: nil)
    
     //Automatic changing of the font size without listening to the previous notification.
     fontHeadline.adjustsFontForContentSizeCategory = true
    
     @objc func methodToBeCalled(notification: Notification) {
    
         //When handling the font size change event, you must redisplay the affected elements.
         fontFootNote.font = UIFont.preferredFont(forTextStyle: .footnote)
     }
    
  • Be careful that the containers fit their contents: using constraints is the best way to perform this task using dynamic values.
  • Don't forget to adapt the color contrast to the text size.

Graphical elements size

Description

Exactly like text, images and tab/tool bar items have a scalable size thanks to accessibility settings but only since iOS11 with Xcode 9.
The example below to illustrate these new features is obtained by following the steps hereafter  :

1. Under Xcode, import the image to be enlarged with a pdf extension and a x1 resolution in the xcassets catalog.

2. In the new Image Set, tick Preserve Vector Data and specify Single Scale as Scales attribute  :


3. If a storyboard is used for this image, tick Adjusts Image Size in the Image View section, otherwise put the adjustsImageSizeForAccessibilityContentSizeCategory image property to true in code  :


4. If a tab bar or a tool bar is used in the application, first repeat the previous 3 steps for each image included in the items to be enlarged in the middle of the screen and then link the image to its appropriate item  :

WARNING : don't forget to check your layout with these new images larger sizes.

Example

An application with a tab bar, whose second bar item displays the Orange logo (added Aspect Fit content mode and constraints to stretch the image view), is created to test the features exposed in the description.

With the Larger Accessibility Sizes activation in the settings (see the previous section), one can easily note in the application  :

  • A larger Orange image size.
  • A larger version of the bar item in an overlay if you touch and hold over it.

Reading order

Description

Redefining the VoiceOver reading order is done using the UIAccessibilityContainer protocol. The idea is to have a table of elements that defines the reading order of the elements. It is often very useful to use the shouldGroupAccessibilityElement attribute so we have a precise order but for a part of the view only (the rest of the view will be read using the native order provided by VoiceOver).

Example

The best way to illustrate this feature is the keyboard whose keys order isn't necessary the appropriate one.
Here's the desired order : 1, 2, 3, 4, 7, 6, 8, 9, 5.


    __weak IBOutlet UIView * blueBlock;
    __weak IBOutlet UIView * greyColumn;

- (void)viewDidAppear:(BOOL)animated {
    [super viewDidAppear:animated];

    //Reads the first three numbers in the grey column.
    greyColumn.shouldGroupAccessibilityChildren = YES;

    //Reads 6, 8, 9 and 5 in this order inside the blue block.
    blueBlock.isAccessibilityElement = NO;
    blueBlock.accessibilityElements = @[key_6,
                                        key_8,
                                        key_9,
                                        key_5];
}

    @IBOutlet weak var greyColumn: UIView!
    @IBOutlet weak var blueBlock: UIView!

    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)

        //Reads the first three numbers in the grey column.
        greyColumn.shouldGroupAccessibilityChildren = true

        //Reads 6, 8, 9 and 5 in this order inside the blue block.
        blueBlock.isAccessibilityElement = false
        blueBlock.accessibilityElements = [key_6,
                                           key_8,
                                           key_9,
                                           key_5]
    }

Date, time and numbers

Description

Using VoiceOver for reading date, time and numbers may become rapidly a headache if some steps fade into obscurity.

Date and time vocalization

The rendering isn't natural if the date or time data are imported text in a label.

Incoming data must be formatted to obtain a natural and understandable descriptive vocalization.


    NSDateFormatter * dateFormatter = [[NSDateFormatter alloc]init];
    [dateFormatter setDateFormat:@"dd/MM/yyyy HH:mm"];

    NSDate * date = [dateFormatter dateFromString:@"01/04/2015 05:30"];

    dateLabel.text = [NSDateFormatter localizedStringFromDate:date
                                                    dateStyle:NSDateFormatterShortStyle
                                                    timeStyle:NSDateFormatterNoStyle];

    dateLabel.accessibilityLabel = [NSDateFormatter localizedStringFromDate:date
                                                                  dateStyle:NSDateFormatterMediumStyle
                                                                  timeStyle:NSDateFormatterNoStyle];


    hourLabel.text = [NSDateFormatter localizedStringFromDate:date
                                                    dateStyle:NSDateFormatterNoStyle
                                                    timeStyle:NSDateFormatterShortStyle];

    NSDateComponents * hourComponents = [[NSCalendar currentCalendar] components:NSCalendarUnitHour | NSCalendarUnitMinute
                                                                        fromDate:date];

    hourLabel.accessibilityLabel = [NSDateComponentsFormatter localizedStringFromDateComponents:hourComponents
                                                                                     unitsStyle:NSDateComponentsFormatterUnitsStyleSpellOut];

    let dateFormatter = DateFormatter()
    dateFormatter.dateFormat = "dd/MM/yyyy HH:mm"

    let date = dateFormatter.date(from: "01/04/2015 05:30")

    dateLabel.text = DateFormatter.localizedString(from: date!,
                                                   dateStyle: .short,
                                                   timeStyle: .none)

    dateLabel.accessibilityLabel = DateFormatter.localizedString(from: date!,
                                                                 dateStyle: .medium,
                                                                 timeStyle: .none)


    hourLabel.text = DateFormatter.localizedString(from: date!,
                                                   dateStyle: .none,
                                                   timeStyle: .short)

    let hourComponents = Calendar.current.dateComponents([.hour, .minute],
                                                         from: date!)
    hourLabel.accessibilityLabel = DateComponentsFormatter.localizedString(from: hourComponents,
                                                                           unitsStyle: .spellOut)

Numbers vocalization

If a number is imported as is in a labeltext, the vocalization will be made on each figure rendering a final value that may be hard to be well understood.

As the previous chapter dealing with date and time, the incoming data must be formatted to be analyzed and vocalized according to the proper value of the explained number.


    NSNumber * numberValue = @54038921.7;

    NSNumberFormatter * numberFormatter = [[NSNumberFormatter alloc]init];
    numberFormatter.numberStyle = NSNumberFormatterDecimalStyle;

    numberLabel.text = [numberFormatter stringFromNumber:numberValue];

    numberLabel.accessibilityLabel = [NSNumberFormatter localizedStringFromNumber:numberValue
                                                                      numberStyle:NSNumberFormatterSpellOutStyle];

    let numberValue = NSNumber(value: 54038921.7)

    numberLabel.text = NumberFormatter.localizedString(from: numberValue,
                                                       number: .decimal)

    numberLabel.accessibilityLabel = NumberFormatter.localizedString(from: numberValue,
                                                                     number: .spellOut)

Phone numbers vocalization

Once more, formatting data is an essential step for a phone number vocalization including the special cases of the "0" figures.
The example hereunder deals with the french dialing but the rationale behind may be applied to any international type of dialing format.
default vocalization is not valid for the following phone number : 06.11.22.33.06
The idea of this format is based on a comma separation of each pair of figures that will provide the vocal punctuation.
in this case the phone number is well vocalized


    NSString * phoneNumberValue = @"06.11.22.33.06";
    NSArray * phoneNumberElts = [phoneNumberValue componentsSeparatedByString:@"."];

    NSNumberFormatter * nbFormatter = [[NSNumberFormatter alloc]init];
    nbFormatter.numberStyle = NSNumberFormatterSpellOutStyle;

    NSMutableString * spelledOutString = [[NSMutableString alloc]init];

    [phoneNumberElts enumerateObjectsUsingBlock:^(id  _Nonnull obj,
                                                  NSUInteger idx,
                                                  BOOL * _Nonnull stop) {
        NSString * elt = (NSString *)obj;

        if (idx != 0) {
            [spelledOutString appendString:@","];
        }

        if ([elt hasPrefix:@"0"]) {

            NSString * firstFigure = [nbFormatter stringFromNumber:@([[elt substringToIndex:1] integerValue])];
            NSString * secondFigure = [nbFormatter stringFromNumber:@([[elt substringFromIndex:1] integerValue])];

            [spelledOutString appendString:firstFigure];
            [spelledOutString appendString:secondFigure];

        } else {
            [spelledOutString appendString:[nbFormatter stringFromNumber:@([elt integerValue])]];
        }
    }];

    phoneNumberLabel.text = phoneNumberValue;
    phoneNumberLabel.accessibilityLabel = spelledOutString;

        let phoneNumberValue = "06.11.22.33.06"
        let phoneNumberElts = phoneNumberValue.components(separatedBy: ".")

        let nbFormatter = NumberFormatter()
        nbFormatter.numberStyle = .spellOut

        var spelledOutString = String()

        for (index, elt) in phoneNumberElts.enumerated() {

            if (index != 0) {
                spelledOutString.append(",")
            }

            if (elt.hasPrefix("0")) {

                let firstFigureValue = Int(String(elt[elt.startIndex]))!
                let firstFigure = nbFormatter.string(from: NSNumber(value:firstFigureValue))
                spelledOutString.append(firstFigure!)

                let secondFigureValue = Int(String(elt[elt.index(elt.startIndex, offsetBy: 1)]))!
                let secondFigure = nbFormatter.string(from: NSNumber(value:secondFigureValue))
                spelledOutString.append(secondFigure!)

            } else {

                let figure = nbFormatter.string(from: NSNumber(value:Int(elt)!))
                spelledOutString.append(figure!)
            }
        }

        phoneNumberLabel.text = phoneNumberValue
        phoneNumberLabel.accessibilityLabel = spelledOutString

Switch Control

Description

The accessibility Switch Control feature revolves around the point mode and the item mode.
accessibility switch control screenshots
The element selection using the item mode works fine when the user interface isn't too complicated and uses native elements.
However, this mode may not be helpful according to the rationale behind some specific use cases and then needs to be customized.

Customization of the item mode

The Xcode InterfaceBuilder shows the structure used for the example hereunder :
xcode screenshot
The following steps represent the customization :

  • Creation of 2 groups {Test_1 + Test_2 ; Btn 5 + Btn 6} that must be selectable in the item mode.
  • Within the other elements, only Btn 1 et Btn 2 must be separately accessible.

@interface ViewController2 ()

@property (weak, nonatomic) IBOutlet UIStackView * btnsParentView;
@property (weak, nonatomic) IBOutlet UIButton * btn1;
@property (weak, nonatomic) IBOutlet UIButton * btn2;
@property (weak, nonatomic) IBOutlet UIButton * btn5;
@property (weak, nonatomic) IBOutlet UIButton * btn6;

@end


@implementation ViewController2
- (void)viewDidAppear:(BOOL)animated {
    [super viewDidAppear:animated];

    //Creation of the first group 'testWrap' COMBINING the 'Test_1' and 'Test_2' buttons.
    UIButton * testOneButton = [self.view viewWithTag:1];
    UIButton * testTwoButton = [self.view viewWithTag:2];
    CGRect testWrapFrame = CGRectUnion(testOneButton.frame, testTwoButton.frame);

    UIAccessibilityElement * testWrap = [[UIAccessibilityElement alloc]initWithAccessibilityContainer:self.view];

    testWrap.isAccessibilityElement = false;
    testWrap.accessibilityFrame = testWrapFrame;
    testWrap.accessibilityNavigationStyle = UIAccessibilityNavigationStyleCombined; //Property specific to Switch Control.
    testWrap.accessibilityElements = @[testOneButton, testTwoButton];


    //Creation of the 'secondGroup' SEPARATING the first two buttons.
    CGRect secondGroupRect = CGRectUnion(_btn1.frame, _btn2.frame);
    CGRect secondGroupFrame = [_btnsParentView convertRect:secondGroupRect
                                                    toView:self.view];
    UIAccessibilityElement * secondGroup = [[UIAccessibilityElement alloc]initWithAccessibilityContainer:_btnsParentView];

    secondGroup.isAccessibilityElement = false;
    secondGroup.accessibilityFrame = secondGroupFrame;
    secondGroup.accessibilityNavigationStyle = UIAccessibilityNavigationStyleSeparate;
    secondGroup.accessibilityElements = @[_btn1, _btn2];


    //Creation of the 'thirdGroup' COMBINING the last two buttons.
    CGRect thirdGroupRect = CGRectUnion(_btn5.frame, _btn6.frame);
    CGRect thirdGroupFrame = [_btnsParentView convertRect:thirdGroupRect
                                                   toView:self.view];
    UIAccessibilityElement * thirdGroup = [[UIAccessibilityElement alloc]initWithAccessibilityContainer:_btnsParentView];

    thirdGroup.isAccessibilityElement = false;
    thirdGroup.accessibilityFrame = thirdGroupFrame;
    thirdGroup.accessibilityNavigationStyle = UIAccessibilityNavigationStyleCombined;
    thirdGroup.accessibilityElements = @[_btn5, _btn6];


    self.view.accessibilityElements = @[testWrap, 
                                        secondGroup, 
                                        thirdGroup];
}
@end

class ViewController: UIViewController {

    @IBOutlet weak var btnsParentView: UIStackView!
    @IBOutlet weak var btn1: UIButton!
    @IBOutlet weak var btn2: UIButton!
    @IBOutlet weak var btn5: UIButton!
    @IBOutlet weak var btn6: UIButton!


    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)

        //Creation of the first group 'testWrap' COMBINING the 'Test_1' and 'Test_2' buttons.
        let testOneButton = self.view.viewWithTag(1) as? UIButton
        let testTwoButton = self.view.viewWithTag(2) as? UIButton
        let testWrapFrame = testOneButton?.frame.union((testTwoButton?.frame)!)

        let testWrap = UIAccessibilityElement(accessibilityContainer: self.view)

        testWrap.isAccessibilityElement = false
        testWrap.accessibilityFrame = testWrapFrame!
        testWrap.accessibilityNavigationStyle = .combined   //Property specific to Switch Control.
        testWrap.accessibilityElements = [testOneButton!, testTwoButton!]


        //Creation of the 'secondGroup' SEPARATING the first two buttons.
        let secondGroupRect = btn1.frame.union(btn2.frame)
        let secondGroupFrame = btnsParentView.convert(secondGroupRect,
                                                      to: self.view)
        let secondGroup = UIAccessibilityElement(accessibilityContainer: btnsParentView)

        secondGroup.isAccessibilityElement = false
        secondGroup.accessibilityFrame = secondGroupFrame
        secondGroup.accessibilityNavigationStyle = .separate
        secondGroup.accessibilityElements = [btn1, btn2]


        //Creation of the 'thirdGroup' COMBINING the last two buttons.
        let thirdGroupRect = btn5.frame.union(btn6.frame)
        let thirdGroupFrame = btnsParentView.convert(thirdGroupRect,
                                                     to: self.view)
        let thirdGroup = UIAccessibilityElement(accessibilityContainer: btnsParentView)

        thirdGroup.isAccessibilityElement = false
        thirdGroup.accessibilityFrame = thirdGroupFrame
        thirdGroup.accessibilityNavigationStyle = .combined
        thirdGroup.accessibilityElements = [btn5, btn6]


        self.view.accessibilityElements = [testWrap,
                                           secondGroup, 
                                           thirdGroup]
    }
}


The visual rendering is exposed hereunder :
visual rendering screenshot
Once activated, the created groups allow to reach directly the elements which they contain.

Continuous adjustable values

Description

Graphics like picker, stepper or slider have the ability to continuously change the value they provide.
picker, stepper and slider screenshot
It's hard to render what's happening when the changing isn't graphically or vocally notified.
The following methodology to resolve this problem for blind people using VoiceOver may be the same for these three graphics, that's why only the stepper will be handled.

Creating a stepper with a label to display its value provides the following rendering :
stepper is vocalize like 3 differents objects
The focus must change to :

  • Get each element that increases or decreases the value.
  • Know the value provided by the label.

Moreover, there is no real time notification dealing with the value changing.
Nothing is definitely blocking in use but these latest remarks lead to a new design for this example that used to be so simple.

The rationale behind is to be able to change the stepper value, to be informed of this modification and to know the new value thanks to a single and unique object.
Group the stepperand the label (a StackView should do the job) then put UIAccessibilityTraitAdjustable as a new trait for this new accessible group.
This trait is MANDATORY linked to the accessibilityIncrement() and accessibilityDecrement() methods that must be implemented to define the continous way of changing the value.

As a result, all the previous constraints are removed and a hint is natively provided by this trait to mention the proper way of using this object.
stepper is well vocalized

  • To get this result, the container class {stepper + label} is first created to allow the delegation for the future value changing.

-===== StepperWrapper.h =====-
NS_ASSUME_NONNULL_BEGIN
@class StepperWrapper;

@protocol AdjustableForAccessibilityDelegate 
- (void)adjustableDecrementForView:(StepperWrapper *)view;
- (void)adjustableIncrementForView:(StepperWrapper *)view;
@end


@interface StepperWrapper : UIStackView
@property(nonatomic,weak) id delegate;
@end
NS_ASSUME_NONNULL_END


-===== StepperWrapper.m =====-
NS_ASSUME_NONNULL_BEGIN
@implementation StepperWrapper

- (instancetype)initWithCoder:(NSCoder *)coder {

    self = [super initWithCoder:coder];

    self.isAccessibilityElement = YES;
    self.accessibilityTraits = UIAccessibilityTraitAdjustable;

    return self;
}

- (void)accessibilityDecrement {
    [_delegate adjustableDecrementForView:self];
}

- (void)accessibilityIncrement {
    [_delegate adjustableIncrementForView:self];
}
@end
NS_ASSUME_NONNULL_END

protocol AdjustableForAccessibilityDelegate: class {
    func adjustableDecrementFor(_ view: StepperWrapper)
    func adjustableIncrementFor(_ view: StepperWrapper)
}


class StepperWrapper: UIStackView {

    weak var delegate: AdjustableForAccessibilityDelegate?

    override init(frame: CGRect) {
        super.init(frame: frame)
    }

    required init(coder: NSCoder) {
        super.init(coder: coder)

        isAccessibilityElement = true
        accessibilityTraits = UIAccessibilityTraitAdjustable
    }

    override func accessibilityDecrement() {
        delegate?.adjustableDecrementFor(self)
    }

    override func accessibilityIncrement() {
        delegate?.adjustableIncrementFor(self)
    }
}
  • Next, the two methods of the implemented protocol must be defined before updating and vocally presenting the new value in the ViewController.

NS_ASSUME_NONNULL_BEGIN
@interface ViewController () 

@property (weak, nonatomic) IBOutlet UIStepper * stepperNoAccess;
@property (weak, nonatomic) IBOutlet UILabel * stepperValueNoAccess;

@property (weak, nonatomic) IBOutlet StepperWrapper * stepperStackViewAccess;
@property (weak, nonatomic) IBOutlet UIStepper * stepperAccess;
@property (weak, nonatomic) IBOutlet UILabel * stepperValueAccess;
@end


@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];

    _stepperStackViewAccess.delegate = self;
    _stepperStackViewAccess.accessibilityLabel = @"Increase or decrease the value";
    _stepperStackViewAccess.accessibilityValue = _stepperValueAccess.text;
}

- (void)adjustableDecrementForView:(StepperWrapper *)view {
    _stepperAccess.value  -= _stepperAccess.stepValue;
    [self updateStepperValue];
}

- (void)adjustableIncrementForView:(StepperWrapper *)view {
    _stepperAccess.value  += _stepperAccess.stepValue;
    [self updateStepperValue];
}

- (void) updateStepperValue {
    _stepperValueAccess.text = [NSString stringWithFormat:@"Value = %0.1f",_stepperAccess.value];
    _stepperStackViewAccess.accessibilityValue = _stepperValueAccess.text;
}
@end
NS_ASSUME_NONNULL_END

class ViewController: UIViewController, AdjustableForAccessibilityDelegate {

    @IBOutlet weak var stepperStackViewAccess: StepperWrapper!
    @IBOutlet weak var stepperAccess: UIStepper!
    @IBOutlet weak var stepperValueAccess: UILabel!


    override func viewDidLoad() {
        super.viewDidLoad()

        stepperStackViewAccess.delegate = self
        stepperStackViewAccess.accessibilityLabel = "Increase or decrease the value"
        stepperStackViewAccess.accessibilityValue = stepperValueAccess.text
    }

    func adjustableDecrementFor(_ view: StepperWrapper) {
        stepperAccess.value -= stepperAccess.stepValue
        updateStepperValue()
    }

    func adjustableIncrementFor(_ view: StepperWrapper) {
        stepperAccess.value += stepperAccess.stepValue
        updateStepperValue()
    }

    private func updateStepperValue() {
        stepperValueAccess.text = "Value = \(stepperAccess.value)"
        stepperStackViewAccess.accessibilityValue = stepperValueAccess.text
    }
}

Custom actions

Description

Some basic gestures may become a real headache to be perfectly understood by VoiceOver in a fluent way for the user.
A convincing example is the iOS native mail that may suggest some actions as follows :
flick left to display actions without VoiceOver
This gesture cannot lead to the proper result with VoiceOver because a flick left will give rise to the selection of the next accessible element instead of suggesting actions as above.

A solution may consist of associating the selected element with an array of actions that will be automatically introduced to the user.


@interface ViewController ()

@property (weak, nonatomic) IBOutlet UILabel * persoElt;

@end


@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];

    UIAccessibilityCustomAction * a11yMoreAction = [[UIAccessibilityCustomAction alloc]initWithName:@"more"
                                                                                                target:self
                                                                                              selector:@selector(moreAction)];
    UIAccessibilityCustomAction * a11yFlagAction = [[UIAccessibilityCustomAction alloc]initWithName:@"flag"
                                                                                                target:self
                                                                                              selector:@selector(flagAction)];
    UIAccessibilityCustomAction * a11yDeleteAction = [[UIAccessibilityCustomAction alloc]initWithName:@"delete"
                                                                                                  target:self
                                                                                                selector:@selector(deleteAction)];

    _persoElt.accessibilityCustomActions = @[a11yMoreAction,
                                             a11yFlagAction,
                                             a11yDeleteAction];
}

- (BOOL)moreAction {
    //Code to be implemented for the appropriate action.
    return YES;
}

- (BOOL)flagAction {
    //Code to be implemented for the appropriate action.
    return YES;
}

- (BOOL)deleteAction {
    //Code to be implemented for the appropriate action.
    return YES;
}
@end

class ViewController: UIViewController {

    @IBOutlet weak var persoElt: UILabel!


    override func viewDidLoad() {
        super.viewDidLoad()

        let a11yMoreAction = UIAccessibilityCustomAction(name: "more",
                                                         target: self,
                                                         selector: #selector(moreAction))

        let a11yFlagAction = UIAccessibilityCustomAction(name: "flag",
                                                         target: self,
                                                         selector: #selector(flagAction))

        let a11yDeleteAction = UIAccessibilityCustomAction(name: "delete",
                                                           target: self,
                                                           selector: #selector(deleteAction))

        persoElt.accessibilityCustomActions = [a11yMoreAction,
                                               a11yFlagAction,
                                               a11yDeleteAction]
    }


    @objc func moreAction() -> Bool {
        //Code to be implemented for the appropriate action.
        return true
    }

    @objc func flagAction() -> Bool {
        //Code to be implemented for the appropriate action.
        return true
    }

    @objc func deleteAction() -> Bool {
        //Code to be implemented for the appropriate action.
        return true
    }
}


The code above gives rise to the following result thanks to consecutive flicks on the selected accessible element :
flick up to vocalize suggesterd actions with VoiceOver activated

Focus an element

Description

The UIAccessibilityFocus informal protocol provides programming elements to be informed of the accessible element focus :

  • accessibilityElementDidBecomeFocused : called when the accessible element is focused.
  • accessibilityElementDidLoseFocus : fired when the accessible element lost focus.
  • accessibilityElementIsFocused : boolean value indicating the accessible element selection.

Overriden inside a view controller, these elements will be helpless if you think they will be called when an accessible element is focused.
However, if they are implemented in the accessible element itself, you won't be disappointed.
This mistake is due to the informal aspect of the protocol that allows an override of its methods inside an inherited NSObject element even if it's not accessible... like a view controller for instance.

The example below enables to follow the focus of an accessible element identified by its accessibleIdentifier.


#import "UIView+focus.h"

@implementation UIView (focus)

- (void)accessibilityElementDidBecomeFocused {

    if ([self accessibilityElementIsFocused]) {
        NSLog(@"My element has become focused.");
    }
}

- (void)accessibilityElementDidLoseFocus {

    if ([self accessibilityElementIsFocused]) {
        NSLog(@"My element has lost focus.");
    }
}

- (BOOL)accessibilityElementIsFocused {

    if ([self.accessibilityIdentifier isEqualToString:@"myAccessibleElt"]) {
        return YES;
    } else {
        return NO;
    }
}
@end

extension UIView {
    override open func accessibilityElementDidBecomeFocused() {

        if self.accessibilityElementIsFocused() {
            print("My element has become focused.")
        }
    }

    override open func accessibilityElementDidLoseFocus() {

        if self.accessibilityElementIsFocused() {
            print("My element has lost focus.")
        }
    }

    override open func accessibilityElementIsFocused() -> Bool {

        if (self.accessibilityIdentifier == "myAccessibleElt") {
            return true
        } else {
            return false
        }
    }
}

Custom rotor

Description

Since iOS10, adding a new rotor option is possible thanks to the UIAccessibilityCustomRotor whose creation is based on 2 elements :

  • UIAccessibilityCustomRotorSearchPredicate : defines the logic according to the flick type on the screen.
  • UIAccessibilityCustomRotorItemResult : ensued element from the logic above.



To illustrate the programing side of this feature, the code snippet below counts and displays all the flicks up and down.


@interface ViewController ()

@property (weak, nonatomic) IBOutlet UILabel * rotorTitle;
@property (weak, nonatomic) IBOutlet UILabel * upLabel;
@property (weak, nonatomic) IBOutlet UILabel * downLabel;

@end


@implementation ViewController

static NSInteger flicksUp;
static NSInteger flicksDown;


+ (void)initialize {

    flicksUp = 0;
    flicksDown = 0;
}


- (void)viewDidLoad {
    [super viewDidLoad];

    UIAccessibilityCustomRotor * rotor = [self buildMyRotor:@"Rotor info"];
    self.accessibilityCustomRotors = @[rotor];
}


- (UIAccessibilityCustomRotor *)buildMyRotor:(NSString * _Nonnull)name{

    return [[UIAccessibilityCustomRotor alloc]initWithName:name
                                           itemSearchBlock:^UIAccessibilityCustomRotorItemResult * _Nullable(UIAccessibilityCustomRotorSearchPredicate * _Nonnull predicate) {

                                               if (predicate.searchDirection == UIAccessibilityCustomRotorDirectionNext) {

                                                   flicksDown += 1;
                                                   self.downLabel.text = [NSString stringWithFormat:@"%ld",(long)flicksDown];

                                               } else {

                                                   flicksUp += 1;
                                                   self.upLabel.text = [NSString stringWithFormat:@"%ld",(long)flicksUp];
                                               }

                                               return [[UIAccessibilityCustomRotorItemResult alloc] initWithTargetElement:self.rotorTitle
                                                                                                              targetRange:nil];
                                           }];
}
@end

class ViewController: UIViewController {

    @IBOutlet weak var rotorTitle: UILabel!

    static var flicksUp = 0
    @IBOutlet weak var upLabel: UILabel!

    static var flicksDown = 0
    @IBOutlet weak var downLabel: UILabel!


    override func viewDidLoad() {
        super.viewDidLoad()

        let rotor = buildMyRotor("Rotor info")
        self.accessibilityCustomRotors = [rotor]
    }


    func buildMyRotor(_ name: String) -> UIAccessibilityCustomRotor {

        return  UIAccessibilityCustomRotor.init(name: name,
                                                itemSearch: { predicate -> UIAccessibilityCustomRotorItemResult? in

                                                    if (predicate.searchDirection == UIAccessibilityCustomRotorDirection.next) {

                                                        ViewController.flicksDown += 1
                                                        self.downLabel.text = String(ViewController.flicksDown)

                                                    } else {

                                                        ViewController.flicksUp += 1
                                                        self.upLabel.text = String(ViewController.flicksUp)
                                                    }

                                                    return UIAccessibilityCustomRotorItemResult.init(targetElement:self.rotorTitle,
                                                                                                     targetRange: nil)
        })
    }
}


The code above gives rise to the following illustrated steps :
changed display with a rotor option
The use of a custom rotor is definitely not a natural part of a mobile application, that's why its functioning and purpose must be fully explained to assist the user experience.

The main difference between a rotor option and a custom action or an adjustable element relies on the fact that it can be activated whatever the selected element.
However, if this selected element is adjustable or holds any custom actions, its actions will prevail over those of the rotor.

Such a feature must be implemented with caution and according to specific needs whose only purpose should be to improve and facilitate the user experience.