Les critères incontournables sous iOS pour le développement

Ce guide a pour objectif de présenter les différentes options du SDK d’accessibilité sous iOS 11 en associant :

  • Des explications détaillées concernant les attributs et méthodes d'accessibilité.
  • Des exemples de code en Swift 4 et en Objective C.


... et des liens vers la documentation officielle d'Apple.

Alternatives textuelles

Description

Sous iOS, la vocalisation d’un élément s’effectue à travers 4 attributs : label, hint, value et trait.
L’ordre de vocalisation est toujours le même (label, value, trait et hint), il ne peut pas être changé et la vocalisation est réalisée en une seule fois à l’arrivée sur l’élément.

Une section de ce guide étant dédiée au trait, nous décrivons ici les 3 autres attributs :

  • accessibilityLabel : le label permet de préciser le titre spécifique à l’accessibilité qui est lu par VoiceOver en lieu et place du texte du composant si celui-ci en possède un, ce qui permet d’avoir un texte de composant plus explicite que celui affiché à l’écran.
    Par exemple, dans le cas d’un bouton dont le titre est « OK », on peut indiquer que le bouton sert à valider un choix.
  • accessibilityValue : la valeur d’un élément est, par défaut, le pourcentage de progression.
    À noter que, pour la plupart des éléments ajustables disponibles dans le SDK, cette value n’a pas besoin d’être précisée car le système restitue automatiquement la valeur à l’utilisateur via VoiceOver.
  • accessibilityHint : le hint permet de décrire le comportement du composant en incorporant des explications supplémentaires.
    Exemple : « cliquez pour obtenir le résultat ».

Ces attributs sont disponibles via l’interface builder de Xcode mais également accessibles en programmation.
Tout élément dérivant de UIView possède ces attributs qui acceptent une chaîne de caractère les rendant subséquemment internationalisables.

Exemple


@interface ChangeTextView() {

    __weak IBOutlet UILabel * monLabel;
    __weak IBOutlet UIProgressView * maProgressView;
}
@end

@implementation ChangeTextView

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

    monLabel.accessibilityLabel = @"bonjour";
    monLabel.accessibilityHint = @"Ceci est un commentaire supplémentaire.";

    maProgressView.accessibilityValue = @"45 pour cent";
}
@end

class ChangeTextView: UIViewController {

    @IBOutlet weak var monLabel: UILabel!
    @IBOutlet weak var maProgressView: UIProgressView!

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

        monLabel.accessibilityLabel = "bonjour"
        monLabel.accessibilityHint = "Ceci est un commentaire supplémentaire."

        maProgressView.accessibilityValue = "45 pour cent"

    }
}

Liens

Nature des éléments

Description

L’attribut accessibilityTraits permet de donner une information à l’API d’accessibilité sur la nature d’un composant.
On peut décider ainsi qu’un item de liste soit pris en compte comme un bouton par VoiceOver car celui-ci est cliquable.
De ce fait, l’accessibilityTrait joue également sur la vocalisation de l’élément car cette nature est restituée par VoiceOver.

Cet attribut d’accessibilité est disponible via l’interface builder de Xcode mais également utilisable directement via le code.

Il existe beaucoup de traits dont les principaux sont fournis ci-dessous :

  • accessibilityTraitNone : supprime toute valeur sémantique à l’élément.
  • accessibilityTraitButton : ajoute le trait « bouton », l’élément est vu comme un bouton par VoiceOver.
  • accessibilityTraitLink : utile pour définir un label en tant que « lien ».
  • accessibilityTraitHeader : permet de définir un élément comme un en-tête (voir la section « titre et en-tête »).
  • accessibilityTraitAdjustable : permet de définir un élément comme un élément « ajustable », c’est-à-dire un élément dont la valeur instantanée peut être modifiée via un geste spécifique de VoiceOver.

Exemple


- (void)customTraits() {
    //Spécification d'un UIPageControl avec le trait ’ajustable’.
    pageControl.accessibilityTraits = UIAccessibilityTraitAdjustable;

    //Ajout d'un en-tête.  
    defaultHeaderViewCell.accessibilityTraits = UIAccessibilityTraitHeader;

    //Combinaison possible de plusieurs traits.  
    onePageButton.accessibilityTraits = UIAccessibilityTraitButton + UIAccessibilityTraitSelected;
}

func customTraits() {
    //Spécification d'un UIPageControl avec le trait ’ajustable’.
    pageControl.accessibilityTraits = UIAccessibilityTraitAdjustable

    //Ajout d'un en-tête. 
    defaultHeaderViewCell.accessibilityTraits = UIAccessibilityTraitHeader

    //Combinaison possible de plusieurs traits. 
    onePageButton.accessibilityTraits = UIAccessibilityTraitButton + UIAccessibilityTraitSelected
}

Lien

Masquer des éléments à l’accessibilité

Description

Il est possible de masquer des éléments aux outils d’accessibilité grâce aux attributs d’accessibilité mais aussi de forcer certains éléments à être visibles pour les outils d’accessibilité uniquement.

  • isAccessibilityElement : booléen qui permet d’indiquer qu’un élément est visible ou non de l’API d’accessibilité (de VoiceOver ou autre).
  • accessibilityElementsHidden : booléen qui permet d’indiquer que les éléments fils de l’élément cible sont visibles ou non de l’API d’accessibilité.
  • accessibilityViewIsModal : booléen qui permet de rendre visible ou non les éléments frères de l’élément cible à l’API d’accessibilité.
    Une explication théorique et une application pratique de cette propriété sont fournies par une vidéo détaillée dans la partie WWDC de ce site.

L’attribut accessibilityElement est disponible via l’interface builder de Xcode mais est également utilisable directement via le code.
Les deux autres attributs sont utilisables uniquement via le code.

Exemple

L'idée est de créer un carré rouge qui va contenir 2 autres carrés (bleu et jaune) pour appliquer les attributs définis précedémment.


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

    //Création d'un élément père dans lequel 2 autres éléments fils vont être insérés.
    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];

    //L'élément père ne doit pas être accessible pour servir de conteneur à ses enfants.
    //Si la valeur est à 'YES', seul cet élément sera accessible sans ses enfants.
    myParentView.isAccessibilityElement = NO;

    //Indication du conteneur que ses enfants peuvent ne pas être accessibles même s'ils sont définis comme tels.
    //Si cette valeur est à 'NO' et la précedénte à 'NO', seuls ces élements seront accessibles.
    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)

        //Création d'un élément père dans lequel 2 autres éléments fils vont être insérés.
        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)

        //L'élément père ne doit pas être accessible pour servir de conteneur à ses enfants.
        //Si la valeur est à 'true', seul cet élément sera accessible sans ses enfants.
        myParentView.isAccessibilityElement = false

        //Indication du conteneur que ses enfants peuvent ne pas être accessibles même s'ils sont définis comme tels.
        //Si cette valeur est à 'false' et la précedénte à 'false', seuls ces élements seront accessibles.
        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
    }

Liens

Déclencher une vocalisation

Description

Il est très facile de déclencher des vocalisations avec VoiceOver.
Attention, nous parlons de vocalisation avec VoiceOver (ce qui implique que VoiceOver soit activé) et pas de TTS (Text To Speech) qui fonctionne indépendamment de l’activation ou non de VoiceOver.

Pour déclencher une vocalisation qui se fera dans la langue du système, il faut envoyer une notification à l’API d’accessibilité via la méthode UIAccessibilityPostNotification avec en paramètres la notification permettant de déclencher une vocalisation UIAccessibilityAnnouncementNotification et la chaîne de caractères à vocaliser.

Exemple


UIAccessibilityPostNotification(UIAccessibilityAnnouncementNotification, @"Message pour la vocalisation.");

UIAccessibilityPostNotification(UIAccessibilityAnnouncementNotification, "Message pour la vocalisation.")

Liens

Connaître l’état des options d’accessibilité

Description

Est-ce que VoiceOver est activé ? Est-ce que le mode audio-mono est activé ?
Plusieurs fonctions du framework UIKit permettent de connaître les statuts de ces options d'accessibilité.
La plus utile est certainement celle qui permet de savoir si VoiceOver est activé au moment de l’appel (UIAccessibilityIsVoiceOverRunning).

D'autres fonctions, peut-être moins utiles à première vue, sont fournies dans les liens ci-après et une présentation très visuelle de certaines d'entre elles est faite lors d'une vidéo WWDC 2018.

Exemple


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

    NSLog(@"VoiceOver vaut %d et SwitchControl vaut %d.", isVoiveOverRunning, isSwitchControlRunning);

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

    print("VoiceOver vaut \(isVoiceOverRunning) et SwichControl vaut \(isSwitchControlRunning).")

Liens

Informer d’une modification sur la page

Description

Lors d’un changement de contenu sur une page, il est possible de notifier l’API d’accessibilité de ce changement à travers différentes notifications.
Pour ce faire, il faut envoyer une notification de modification à l’API d’accessibilité via la méthode UIAccessibilityPostNotification.

Il existe plusieurs notifications de modification, mais les deux plus utiles sont :

  • UIAccessibilityLayoutChangedNotification : permet de spécifier à l’API d’accessibilité qu’une partie de la page a été modifiée et doit être accompagné d'un NSString ou d'un UIObject.
    Avec un NSString, la notification se comporte comme une UIAccessibilityAnnouncementNotification et lance une vocalisation VoiceOver.
    Avec un UIObject, le focus est repositionné sur l’élément précisé.
    Cette notification est très similaire à UIAccessibilityAnnouncementNotification mais son utilisation doit être mise en avant dès lors qu'une modification dynamique du contenu se produit.
  • UIAccessibilityScreenChangedNotification : permet d’annoncer un changement global de la page et accepte soit nil, soit l’élément qui doit recevoir le focus.
    Avec nil, la notification vocalise et sélectionne le premier élément accessible de la page.
    Avec un UIObject, le focus est repositionné sur l’élément précisé en lançant une vocalisation VoiceOver.
    Le son utilisé pour notifier la modification est similaire à l'arrivée d'une nouvelle page.

Exemple


//L'élément 'myLabel' est sélectionné et vocalisé avec sa nouvelle valeur.
- (IBAction)tapHere:(UIButton *)sender {

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

//Le premier élément accessible de la page est sélectioné et vocalisé avec un son spécifique.
- (IBAction)clic:(UIButton *)sender {

    UIAccessibilityPostNotification(UIAccessibilityScreenChangedNotification, nil);
}

//L'élément 'myLabel' est sélectionné et vocalisé avec sa nouvelle valeur.
@IBAction func clicHere(_ sender: UIButton) {

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

//Le premier élément accessible de la page est sélectioné et vocalisé avec un son spécifique.
@IBAction func clic(_ sender: UIButton) {

    UIAccessibilityPostNotification(UIAccessibilityScreenChangedNotification, mySecondLabel)
}

Liens

Modifier la langue de vocalisation

Description

Afin de modifier la langue de prononciation de VoiceOver pour un mot ou un texte, il existe l’attribut accessibilityLanguage .
Disponible via le protocole UIAccessibility, cet attribut permet de redéfinir la langue de prononciation d’un texte.
Par exemple, si on utilise cet attribut sur un UILabel, alors celui-ci sera vocalisé par VoiceOver dans la nouvelle langue donnée en valeur de l’attribut.

Exemple


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

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

@IBAction func tapHere(_ sender: UIButton) {

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

Lien

Modifier la zone de focus de VoiceOver

Description

Dans le cas d’objet modifié dynamiquement ou d’élément ne dérivant pas de UIView, il est possible de déterminer la zone géographique d’accessibilité de cet élément, c’est-à-dire la zone que VoiceOver met en surbrillance lors du focus.

  • accessibilityFrame : permet de définir cette zone via un rectangle (CGRect).
    Par défaut pour un élément dérivant de UIView, cette zone est la partie « visible » de la vue.
  • accessibilityPath : équivalent à AccessibilityFrame mais permet de définir la zone via des courbes de Bézier.
  • accessibilityActivationPoint : définit un point de contact au sein de la frame dont l'action résultante sera activée par une sélection classique d'élément via un double tap.
    Par défaut, ce point se trouve au centre de la frame mais on peut le définir n'importe à l'intérieur de cette dernière, l'idée étant de pouvoir activer un élement facilement lors d'un regroupement par exemple.

    En conservant la valeur par défaut de ce point, on peut aisément se retrouver dans une situation où on active involontairement l'élément situé au milieu de la frame uniquement en activant le regroupement créé.

Exemple


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;

}

//Première façon d'augmenter la zone de focus.
- (IBAction)tapHere:(UIButton *)sender {

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

    UIAccessibilityPostNotification(UIAccessibilityLayoutChangedNotification, myLabel);
}

//Seconde façon d'augmenter la zone de focus (Bézier).
- (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)
    }

Liens

Grouper des éléments

Description

On peut envisager de grouper des éléments pour vocaliser en une seule fois l'ensemble formé et associer au groupe ainsi créé une action dédiée par exemple.
Dès lors, les éléments encapsulés ne doivent plus être considérés comme accessibles car seul leur conteneur doit être perçu comme tel.

Exemple 1

Nous avons un 'label' et un 'switch control' que nous souhaitons regrouper et traiter d'un seul bloc.
Dans ce cas, on va créer une vue qui va englober les éléments impactés puis implémenter une action qui va indiquer l'action à réaliser en cas d'activation de la zone par l'utilisateur.

Création de l'élément accessible qui va regrouper les éléments souhaités :


#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];

    //Création de la vue qui va encapsuler le 'label' et le '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)

        //Création de la vue qui va encapsuler le 'label' et le 'Switch Control'.
        let wrap = MyWrapView.init(with: myLabel,
                                   and: mySwitch)

        self.view.addSubview(wrap)
    }
}


... et implémentation de la classe utilisée pour définir de façon précise l'action à associer au double tap d'activation :


@implementation MyWrapView

//Index utilisés pour repérer les éléments accessibles dans la vue de regroupement.
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;
}


//Fonction appelée par le système quand un double tap est réalisé sur l'élément sélectionné pour l'activer.
- (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 {

    //Index utilisés pour repérer les éléments accessibles dans la vue de regroupement.
    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) ? "activé" : "désactivé"

        self.isAccessibilityElement = true
        self.accessibilityLabel = "le contrôle est" + switchValue.description
        self.accessibilityHint = "tapez deux fois pour changer sa valeur."
    }


    //Fonction appelée par le système quand un double tap est réalisé sur l'élément sélectionné pour l'activer.
    override func accessibilityActivate() -> Bool {

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

        let switchValue = (theSwitch?.isOn)! ? "activé" : "désactivé"

        self.accessibilityLabel = "le contrôle est" + switchValue.description

        return true
    }
}

Exemple 2

Nous avons un 'label', un 'switch control' et un bouton que nous souhaitons regrouper en un seul bloc dont l'activation changera automatiquement l'état du 'switch control' sans avoir à définir une action comme précédemment.
L'idée la plus simple consisterait à placer le 'switch control' au milieu de la nouvelle frame créée de façon à avoir son accessibilityActivationPoint directement dessus.
Cela n'étant malheureusement pas toujours possible, il va donc falloir créer un élément accessible qui regroupera tous les objets impactés puis définir son accessibilityActivationPoint sur le '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 = @"regroupement d'éléments";
    elt.accessibilityHint = @"tapez deux fois pour modifier le switch";
    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 = "regroupement d'éléments"
        elt.accessibilityHint = "tapez deux fois pour modifier le switch"
        elt.accessibilityFrameInContainerSpace = a11yEltFrame
        elt.accessibilityActivationPoint = mySwitch.center

        self.view.accessibilityElements = [elt]
    }
}

Une autre possibilité de groupement d'éléments pourrait utiliser l’attribut shouldGroupAccessibilityChildren, booléen qui permet d’indiquer à VoiceOver qu’il doit grouper les enfants de la vue qui porte l’attribut.
Cela permet notamment de faire des vocalisations uniques ou de définir un ordre de lecture VoiceOver particulier pour une partie de la page seulement (voir la section Ordre de lecture).

Liens

Événements d’accessibilité

Description

Le système iOS envoie un certain nombre d’événements d’accessibilité à destination des applications lors de la modification des options d’accessibilité.
Par exemple, si VoiceOver est désactivé durant l’utilisation de l’application, celle-ci recevra l’événement UIAccessibilityVoiceOverStatusDidChange, ce qui peut être très utile couplé à la fonction UIAccessibilityIsVoiceOverRunning grâce à laquelle on peut exécuter un traitement particulier quand VoiceOver est activé.
Mais que se passe-t-il si VoiceOver est désactivé alors que ce traitement a déjà eu lieu ?

C’est là que les événements système peuvent être utilisés et, en restant à leur écoute, il est possible d’appliquer des traitements spécifiques de manière dynamique.

Exemple

Dans cet exemple, on appelle une méthode spécifique au moment où le statut de VoiceOver ou du Switch Control change.


- (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).")
    }

Lien

Tous les événements sont disponibles sur la documentation officielle d'Apple.

Taille des textes

Description

Depuis iOS7, il est possible de modifier dynamiquement la taille des textes d'une application à l’aide du paramétrage du téléphone.

Quelques points sont néanmoins essentiels pour la bonne utilisation de l'API mise à disposition :

  • Utiliser la police système pour les textes de l’application afin de se faciliter grandement la tâche même si l'utilisation d'autres polices est devenue nettement plus aisée depuis l'arrivée de UIFontMetrics avec iOS11.

    
     __weak IBOutlet UILabel * fontHeadline;
     __weak IBOutlet UILabel * fontFootNote;
    
     //Utilisation de la font native pour le titre principal d'un page.
     UIFont * myFont = [UIFont preferredFontForTextStyle:UIFontTextStyleHeadline];
    
     //Définition de la font pour le titre principal d'une page.
     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!
    
     //Utilisation de la police native par défaut pour le titre principal d'une page.
     let myFont = UIFont.preferredFont(forTextStyle: .headline)
    
     //Définition personnalisée de la police pour le titre principal d'une page.
     let fontHead = UIFont(name: "Chalkduster", size: 30.0)
     let fontHeadMetrics = UIFontMetrics(forTextStyle: .headline)
     fontHeadline.font = fontHeadMetrics.scaledFont(for: fontHead!)
    
  • Penser à écouter la notification UIContentSizeCategoryDidChange qui annonce le changement de la taille du texte à partir des paramètres du téléphone.
    Cette tâche est simplifiée depuis iOS10 où l'attribut adjustsFontForContentSizeCategory se charge de la mise à jour automatique de la nouvelle taille de la police système au sein de l'application (cet attribut ne peut s'appliquer aux polices personnalisées qu'avec l'utilisation de UIFontMetrics en iOS11).
    Il est aussi possible d'utiliser la méthode traitCollectionDidChange du protocole informel UITraitEnvironment qui sera automatiquement appelée dès qu'une modification concernant l'environnement de l'interface iOS surviendra (class/content size, portrait/paysage).

    
     //Écoute de la notification annonçant le changement de taille de la police.
     [[NSNotificationCenter defaultCenter] addObserver:self
                                              selector:@selector(methodToBeCalled:)
                                                  name:UIContentSizeCategoryDidChangeNotification
                                                object:nil];
    
     //Modification automatique de la taille de la police sans utiliser la notification.
     fontHeadline.adjustsFontForContentSizeCategory = YES;
    
     - (void)methodToBeCalled:(NSNotification *)notification {
    
         //Il faut de nouveau affecter la police des éléments impactés lors du traitement de cette notification.
         fontFootNote.font = [UIFont preferredFontForTextStyle:UIFontTextStyleFootnote];
     }
    
    
     //Écoute de la notification annonçant le changement de taille de la police.
     NotificationCenter.default.addObserver(self,
                                            selector:#selector(methodToBeCalled(notification:)),
                                            name: .UIContentSizeCategoryDidChange,
                                            object: nil)
    
     //Modification automatique de la taille de la police sans utiliser la notification.
     fontHeadline.adjustsFontForContentSizeCategory = true
    
     @objc func methodToBeCalled(notification: Notification) {
    
         //Il faut de nouveau affecter la police des éléments impactés lors du traitement de cette notification.
         fontFootNote.font = UIFont.preferredFont(forTextStyle: .footnote)
     }
    
  • Ne pas oublier d'adapter les contraintes graphiques aux éléments susceptibles de voir leur taille modifiée en privilégiant l'utilisation de valeurs dynamiques.
  • Penser à adapter le contraste des couleurs à la taille de texte modifiée si nécessaire.

Liens

Taille des éléments graphiques

Description

Tout comme la taille des textes est adaptable selon les réglages d'accessibilité (voir la rubrique précédente), la taille des images ainsi que celle des éléments d'une barre de tabulation ou d'outils l'est aussi mais uniquement depuis iOS11 avec Xcode 9.

En suivant les différentes étapes ci-dessous, vous obtiendrez l'effet défini précédemment et présenté graphiquement en exemple à la fin de cette rubrique :

1. Sous Xcode, importer l'image à grossir au format pdf à la résolution x1 dans le catalogue xcassets.

2. Dans l'Image Set qui vient d'être créé, cocher la case Preserve Vector Data et spécifier Single Scale :


3. Si un storyboard est utilisé pour intégrer l'image, cocher Adjusts Image Size dans la partie Image View, sinon mettre à true la propriété adjustsImageSizeForAccessibilityContentSizeCategory de l'image si cette opération se fait de façon programmatique :


4. Si une barre de tabulation ou une barre d'outils est aussi à intégrer dans l'application, renouveler les 3 étapes précédentes pour chacune des images à utiliser en grossissement de l'onglet au milieu de l'écran puis associer l'image souhaitée aux différents éléments impactés :

ATTENTION : s'assurer que les contraintes mises en place initialement permettent toujours un affichage cohérent et désiré après grossissement.

Exemple

De façon à pouvoir tester à la fois le grossissement des images et celui d'un onglet sélectionné, on crée une application contenant une barre de tabulations contenant 2 onglets dont seul le second nous intéresse et affiche l'image du logo Orange.

Après modification du grossissement de texte dans les réglages (voir la rubrique précédente), on revient dans l'application pour constater :

  • Une taille de l'image Orange nettement plus conséquente.
  • Au milieu de l'écran, l'affichage grossi de l'onglet sur lequel on doit appuyer de façon continue pour provoquer cettte apparition.

Lien

Ordre de lecture

Description

Redéfinir l’ordre de lecture pour VoiceOver s’effectue en respectant le protocole UIAccessibilityContainer.
L’idée est d’avoir un tableau des éléments de la vue qui définit l’ordre de lecture des éléments.
Il est bien souvent nécessaire d’utiliser l’attribut shouldGroupAccessibilityElement afin d’avoir un ordre précis mais pour une partie seulement de la vue (le reste étant l’ordre naturel de lecture proposé par VoiceOver).

Exemple

Le meilleur exemple pour illustrer cette fonctionnalité est le clavier pour lequel les touches sucessives ne suivent pas forcément l'ordre natif proposé par VoiceOver.
Dans cet exemple, on veut l'ordre suivant : 1, 2, 3, 4, 7, 6, 8, 9, 5.
On crée les 2 vues grise et bleue au sein desquelles on incorpore les chiffres appropriés comme défini ci-dessous :
affichage des vues grise et bleue pour l'exemple


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

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

    //Lecture des 3 premiers nombres dans la vue grise.
    greyColumn.shouldGroupAccessibilityChildren = YES;

    // Lecture des chiffres 6, 8, 9 et 5 au sein du bloc bleu.
    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)

        //Lecture des 3 premiers nombres dans la vue grise.
        greyColumn.shouldGroupAccessibilityChildren = true

        // Lecture des chiffres 6, 8, 9 et 5 au sein du bloc bleu.
        blueBlock.isAccessibilityElement = false
        blueBlock.accessibilityElements = [key_6,
                                           key_8,
                                           key_9,
                                           key_5]
    }

Liens

Date, heure et nombres

Description

La lecture des date, heure et nombres n'est pas compliquée mais peut très vite devenir un réel casse-tête avec VoiceOver .

Lecture des dates et des heures

Si on met directement sous forme de texte la date ou l'heure dans le label, on s'aperçoit rapidement que le rendu n'est pas naturel à l'écoute.

Il faut absolument formater les données en entrée pour obtenir une vocalisation descriptive naturelle et compréhensible.


    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)

Lecture des nombres

En indiquant la valeur d'un nombre directement dans le texte d'un label, la vocalisation se fera sur chacun des chiffres présentés rendant la véritable valeur du nombre difficile à deviner.

Comme pour les date et heure, il faut formater la donnée en entrée pour qu'elle puisse être analysée et vocalisée selon la véritable valeur du nombre qu'elle représente.


    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)

Lecture des numéros de téléphone

La problématique liée à la vocalisation d'un numéro de téléphone est identique à celle des nombres puisqu'elle s'appuie entièrement sur le formatage à appliquer avec une prise en compte particulière des chiffres "0".
L'exemple donné ci-dessous concerne la numérotation française avec une logique qui peut se décliner à n'importe quel type de format de numérotation.

L'idée est de séparer chaque paire de chiffres par une virgule qui va fournir la ponctuation vocale.


    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

Liens

Contrôle de sélection

Description

L'utilisation du contrôle de sélection s'articule autour du mode point et du mode élément définis ci-dessous.
modes point et élément pour le contrôle de sélection
La sélection des éléments avec le mode élément fonctionne globalement bien quand les éléments proposés sont natifs et que l'application n'est pas trop compliquée graphiquement.
Il peut très bien arriver que ce mode de sélection ne suive pas la logique souhaitée et ne propose pas les éléments dans l'ordre désiré.

Personnalisation du mode élément

La structure utilisée pour l'exemple est présentée ci-dessous grâce à l'InterfaceBuilder de Xcode :
exemple de l'interface graphique de xcode
Afin de personnaliser la sélection de ces éléments, on souhaite :

  • Créer 2 groupes {Test_1 + Test_2 ; Btn 5 + Btn 6} sélectionnables en mode élément.
  • Avoir uniquement les éléments restants Btn 1 et Btn 2 accessibles séparément.

@interface ViewController ()

@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 ViewController
- (void)viewDidAppear:(BOOL)animated {
    [super viewDidAppear:animated];

    //Création du premier groupe 'testWrap' en COMBINANT les boutons 'Test_1' et 'Test_2'.
    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 specifique au contrôle de sélection.
    testWrap.accessibilityElements = @[testOneButton, testTwoButton];


    //Création du second groupe 'secondGroup' en SÉPARANT les boutons 1 et 2.
    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];


    //Création du troisième groupe 'thirdGroup' en COMBINANT les boutons 5 et 6.
    CGRect thirdGroupRect = CGRectUnion(_btn1.frame, _btn2.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)

        //Création du premier groupe 'testWrap' en COMBINANT les boutons 'Test_1' et 'Test_2'.
        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 specifique au contrôle de sélection.
        testWrap.accessibilityElements = [testOneButton!, testTwoButton!]


        //Création du second groupe 'secondGroup' en SÉPARANT les boutons 1 et 2.
        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]


        //Création du troisième groupe 'thirdGroup' en COMBINANT les boutons 5 et 6.
        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]
    }
}


Le rendu de ce code est visualisable ci-dessous :
rendu final avec le code implémenté
Les groupes créés permettent d'accéder directement aux éléments qu'ils contiennent dès qu'ils sont activés.

Lien

Valeurs continûment ajustables

Description

Des éléments graphiques comme le picker, le stepper ou encore le slider permettent de changer de façon continue la valeur qu'ils proposent de modifier.

Quand on ne voit pas la modification dynamique se faire ou qu'on n'en est pas informé vocalement, il devient très compliqué de pouvoir se rendre compte de ce qui se passe.
La méthodologie utilisée pour solutionner cette problématique pour une personne non voyante utilisant VoiceOver reste la même pour ces trois éléments, c'est pourquoi seul le cas du stepper sera traité.

L'implémentation de cet objet graphique est relativement simple mais son utilisation avec VoiceOver requiert quelques ajustements pour obtenir un meilleur parcours utilisateur.
Si on crée un stepper auquel on ajoute un label pour afficher sa valeur, on obtient le résultat suivant :
exemple de stepper sans une bonne implémentation
À partir de là, on s'aperçoit que le focus doit être déplacé pour :

  • Atteindre chacun des deux éléments permettant d'augmenter ou de diminuer la valeur.
  • Connaître la valeur obtenue via le label.

De plus, il n'y a aucune indication de changement de la valeur en temps réel.
Certes, rien n'est bloquant mais, si l'on souhaite réellement mettre en place cet objet avec un rendu le plus fluide possible, ces quelques remarques conduisent tout naturellement à concevoir différemment cet exemple pourtant si simple.

L'idée est de pouvoir changer la valeur du stepper, être informé de son changement et d'en connaître la valeur par le biais d'un unique objet.
Il faut donc regrouper le stepper et le label (à l'aide d'une StackView par exemple) puis associer UIAccessibilityTraitAdjustable à ce nouveau groupe accessible.
Ce nouveau trait va permettre de modifier de façon continue la valeur de l'objet auquel il est associé en implémentant OBLIGATOIREMENT les méthodes accessibilityIncrement() et accessibilityDecrement().

On élimine ainsi toutes les contraintes rencontrées initialement et on obtient, en plus, un hint lié à ce nouveau trait qui indique la manipulation nécessaire au bon fonctionnement.

  • Pour aboutir à ce résultat, on définit tout d'abord une classe conteneur {stepper + label} qui va permettre la délégation pour la modification ultérieure de la valeur.

-===== 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)
    }
}
  • Ensuite, il faut redéfinir les 2 méthodes du protocole implémenté pour indiquer ce qu'elles doivent réaliser avant de mettre à jour la valeur modifiée et de la présenter vocalement dans le 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 = @"Compteur pour adapter la valeur";
    _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:@"Valeur = %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 = "Compteur pour adapter la valeur"
        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 = "Valeur = \(stepperAccess.value)"
        stepperStackViewAccess.accessibilityValue = stepperValueAccess.text
    }
}

Lien

Actions personnalisées

Description

Certaines manipulations basiques peuvent devenir un vrai casse-tête pour se fondre dans une navigation sereine avec VoiceOver et se transformer en éléments parfaitement accessibles.
Un exemple probant est celui du mail iOS natif qui permet d'accéder à un ensemble d'actions comme le montre le schéma suivant :
accès aux actions d'un mail sans voiceover avec un balayage gauche
La gestuelle utilisée graphiquement ne peut convenir à VoiceOver : un balayage vers la gauche sélectionnerait l'élément accessible suivant au lieu de proposer les choix avancés dans l'exemple précédent.

Une solution consiste à associer à l'élément sélectionné un tableau d'actions dont le système se chargera d'indiquer automatiquement la présence en informant vocalement l'utilisateur de leur disponibilité.


@interface ViewController ()

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

@end


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

    UIAccessibilityCustomAction * a11yOptionsAction = [[UIAccessibilityCustomAction alloc]initWithName:@"options"
                                                                                                target:self
                                                                                              selector:@selector(optionsAction)];
    UIAccessibilityCustomAction * a11yDrapeauAction = [[UIAccessibilityCustomAction alloc]initWithName:@"drapeau"
                                                                                                target:self
                                                                                              selector:@selector(drapeauAction)];
    UIAccessibilityCustomAction * a11yCorbeilleAction = [[UIAccessibilityCustomAction alloc]initWithName:@"corbeille"
                                                                                                  target:self
                                                                                                selector:@selector(corbeilleAction)];

    _persoElt.accessibilityCustomActions = @[a11yOptionsAction,
                                             a11yDrapeauAction,
                                             a11yCorbeilleAction];
}

- (BOOL)optionsAction {
    //Code à implémenter pour cette action.
    return YES;
}

- (BOOL)drapeauAction {
    //Code à implémenter pour cette action.
    return YES;
}

- (BOOL)corbeilleAction {
    //Code à implémenter pour cette action.
    return YES;
}
@end

class ViewController: UIViewController {

    @IBOutlet weak var persoElt: UILabel!


    override func viewDidLoad() {
        super.viewDidLoad()

        let a11yOptionsAction = UIAccessibilityCustomAction(name: "options",
                                                            target: self,
                                                            selector: #selector(optionsAction))

        let a11yDrapeauAction = UIAccessibilityCustomAction(name: "drapeau",
                                                            target: self,
                                                            selector: #selector(drapeauAction))

        let a11yCorbeilleAction = UIAccessibilityCustomAction(name: "corbeille",
                                                              target: self,
                                                              selector: #selector(corbeilleAction))

        persoElt.accessibilityCustomActions = [a11yOptionsAction,
                                               a11yDrapeauAction,
                                               a11yCorbeilleAction]
    }


    @objc func optionsAction() -> Bool {
        //Code à implémenter pour cette action.
        return true
    }

    @objc func drapeauAction() -> Bool {
        //Code à implémenter pour cette action.
        return true
    }

    @objc func corbeilleAction() -> Bool {
        //Code à implémenter pour cette action.
        return true
    }
}


Le code implémenté ci-dessus permet d'obtenir le résultat suivant par balayages successifs sur l'élément accessible utilisé :
accès aux actions avec voiceover en utilisant un balayage vers le haut

Liens

Focus d'un élément

Description

Le protocole informel UIAccessibilityFocus fournit des éléments de programmation efficaces de façon à pouvoir être informé d'une sélection passé, active ou à venir d'un élément accessible :

  • accessibilityElementDidBecomeFocused : méthode appelée dès que l'élément accessible est sélectionné.
  • accessibilityElementDidLoseFocus : méthode appelée dès que l'élément accessible perd le focus.
  • accessibilityElementIsFocused : valeur booléenne qui permet de savoir si un élément accessible est sélectionné.

Attention, ces méthodes ne sont pas appelées au sein d'un contrôleur de vue si un de ses élements accessibles est sélectionné mais uniquemnet si elles sont implémentées sur l'élément accessible lui-même.
Cette erreur à laquelle on ne pense pas de prime abord provient du caractère informel du protocole UIAccessibilityFocus dont les éléments peuvent subir un override sur tout objet héritant de NSObject même s'il n'est pas accessible... comme un contrôleur de vue par exemple.

L'exemple de code ci-dessous permet de suivre le focus d'un élément accessible identifié par son accessibleIdentifier.


#import "UIView+focus.h"

@implementation UIView (focus)

- (void)accessibilityElementDidBecomeFocused {

    if ([self accessibilityElementIsFocused]) {
        NSLog(@"Mon élément est sélectionné.");
    }
}

- (void)accessibilityElementDidLoseFocus {

    if ([self accessibilityElementIsFocused]) {
        NSLog(@"Mon élément a perdu le focus.");
    }
}

- (BOOL)accessibilityElementIsFocused {

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

extension UIView {
    override open func accessibilityElementDidBecomeFocused() {

        if self.accessibilityElementIsFocused() {
            print("Mon élément est sélectionné.")
        }
    }

    override open func accessibilityElementDidLoseFocus() {

        if self.accessibilityElementIsFocused() {
            print("Mon élément a perdu le focus.")
        }
    }

    override open func accessibilityElementIsFocused() -> Bool {

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

Lien

Rotor personnalisé

Description

Depuis iOS10, il est possible d'ajouter une action spécifique au rotor de VoiceOver en s'appuyant sur l'objet UIAccessibilityCustomRotor dont la construction prend en compte 2 éléments principaux en entrée :

  • UIAccessibilityCustomRotorSearchPredicate : définit la logique à mettre en oeuvre selon le type de balayage effectué sur l'écran.
  • UIAccessibilityCustomRotorItemResult : correspond à l'élément issu de la logique précedente.



Le code fourni ci-dessous permet de compter et d'afficher le nombre de balayages haut et bas (finalité inutile avec le rotor mais qui permet de mettre en avant sa création programmatique).


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


Le code implémenté ci-dessus permet d'obtenir le résultat suivant :
affichage modifié avec une action du rotor
L'utilisation d'un rotor personnalisé n'est pas du tout naturelle au sein d'une application, c'est pourquoi il est primordial de bien annoncer son fonctionnement et sa finalité pour faciliter au maximum l'expérience utilisateur.

La majeure différence du rotor avec les actions personnalisées ou encore les valeurs continûment ajustables réside dans sa possible utilisation quel que soit l'élément sélectionné sur l'écran.
Cependant, si l'élément sélectionné est ajustable ou contient des actions personnalisées, ses actions prévaudront sur celles du rotor.

L'implémentation d'une telle fonctionnalité au sein d'une application est donc à envisager selon des besoins bien spécifiques dont le seul objectif doit être de faciliter l'expérience utilisateur.

Liens