fabernovel loader

Apr 7, 2017 | 14 min de lecture

Tech

L'accessibilité des applications iOS

iOS Accessibility
iOS Accessibility

Pierre Felgines

Developer Senior


FABERNOVEL TECHNOLOGIES
Pour une de nos dernières applications nous avions une exigence forte de la part du client. L’application devait être entièrement accessible. Qu’est-ce que cela veut dire en pratique ? Cela signifie que les personnes atteintes d’invalidités visuelles, auditives ou physiques peuvent utiliser l’application et ses fonctionnalités comme n’importe quel autre utilisateur.

Depuis le début d’iOS, Apple a mis en place plusieurs outils pour l’accessibilité :

  • Zoom, qui permet de grossir l’écran
  • Police Dynamique, qui laisse le choix à l’utilisateur de la taille du texte
  • Inversion des couleurs (pour les daltoniens)
  • Mono Audio, qui combine les canaux stéréos gauche et droit sur un seul canal
  • Speak Auto-text, qui énonce les corrections grammaticales et les suggestions quand l’utilisateur écrit
  • Voice Control, qui permet de passer des appels téléphoniques ou de contrôler la musique avec des commandes vocales
  • VoiceOver, qui énonce les différents éléments de l’écran

Dans ce billet, nous allons nous intéresser aux invalidités visuelles et notamment à deux fonctionnalités : Police Dynamique et VoiceOver. Nous détaillerons leur utilisation et présenterons les difficultés que nous avons rencontrées.

Police Dynamique

Vous pouvez choisir la taille du texte dans Réglages > Général > Accessibilité > Police plus grande.

Exemple de différentes tailles de police
Exemple de différentes tailles de police

Quand on change la valeur du slider, le système et toutes les applications qui supportent la Police Dynamique vont mettre à jour leurs tailles de police.

Pour supporter la Police Dynamique, il faut observer la notification NSNotification.Name.UIContentSizeCategoryDidChange et faire en sorte que chaque vue dans la hiérarchie réponde à cette mise à jour de taille.

Voici le code que nous utilisons :

protocol ContentSizable {
    func ad_updateContentSize()
}

extension UIView : ContentSizable {
    func ad_updateContentSize() {
        subviews.forEach { $0.ad_updateContentSize() }
    }
}

extension UIViewController : ContentSizable {
    func ad_updateContentSize() {
        view.ad_updateContentSize()
    }
}

// This protocol allow to start/end observing `NSNotification.Name.UIContentSizeCategoryDidChange`
// `UIViewController` provides default implementation
protocol ContentSizeObservable {
    func startObservingContentSize()
    func endObservingContentSize()
}

extension UIViewController : ContentSizeObservable {
    func startObservingContentSize() {
        NotificationCenter.default.addObserver(
            self,
            selector: #selector(preferredContentSizeChanged),
            name: NSNotification.Name.UIContentSizeCategoryDidChange,
            object: nil
        )
    }

    func endObservingContentSize() {
        NotificationCenter.default.removeObserver(
            self,
            name: NSNotification.Name.UIContentSizeCategoryDidChange,
            object: nil
        )
    }

    @objc private func preferredContentSizeChanged() {
        ad_updateContentSize()
    }
}

On peut voir que lorsque la notification est interceptée, le viewController a l’opportunité de mettre à jour son contenu, puis laisse sa vue ainsi que ses sous-vues gérer la notification. Cela signifie que chaque vue ou viewController doit override la méthode ad_updateContentSize() et actualiser les polices des labels ou des boutons en fonction. Cette méthode sera appelée dès que le système émet une notification de changement de taille.

Voici comment mettre à jour la police d’un label :

class CustomTableViewCell : UITableViewCell {

    @IBOutlet var titleLabel: UILabel!

    override func ad_updateContentSize() {
        super.ad_updateContentSize()
        titleLabel.font = UIFont.preferredCustomFont(forTextStyle: .body)
    }
}

Notons que dans le code précédent, nous utilisons la méthode preferredCustomFont pour récupérer la police à utiliser. C’est parce que nous voulons utiliser la fonctionnalité de Police Dynamique avec une police non gérée nativement par le système. Si l’on veut utiliser la police par défaut, il faut utiliser la méthode UIFont.preferredFont(forTextStyle: .body). La liste des différentes tailles de police pour chaque style est présentée dans la documentation iOS Human Interface Guidelines.

Voici un exemple de code qui montre comment utiliser une police non native avec la Police Dynamique :

extension UIFont {

    class func preferredCustomFont(forTextStyle style: UIFontTextStyle) -> UIFont {
        let font = preferredFont(forTextStyle: style) // default font
        let size = font.pointSize

        if style == UIFontTextStyle.headline || style == UIFontTextStyle.subheadline {
            return boldFont(size: size)
        } else {
            return regularFont(size: size)
        }
    }

    class func boldFont(size: CGFloat) -> UIFont {
        guard let font = UIFont(name: "CustomFontName-Bold", size: size) else {
            return boldSystemFont(ofSize: size)
        }
        return font
    }
  
    class func regularFont(size: CGFloat) -> UIFont {
        guard let font = UIFont(name: "CustomFontName-Regular", size: size) else {
            return systemFont(ofSize: size)
        }
        return font
    }
}

La fonctionnalité de Police Dynamique est simple à intégrer et doit simplifier la manière dont les designers pensent la typographie. En effet, il ne faut plus raisonner en terme de police et de taille, mais en terme de style hiérarchique de police (.body , .headline, …).

VoiceOver

Une autre fonctionnalité très importante de l’accessibilité sur iOS est VoiceOver. VoiceOver permet de lire une description audio des différents éléments à l’écran ou des actions que l’utilisateur peut effectuer.
La difficulté avec VoiceOver est de fournir du contenu significatif, c’est-à-dire des informations précises et utiles sur les éléments à l’écran : position, description, comportement, valeur, type.

L’accessibilité comme interface utilisateur

Avant de commencer à intégrer VoiceOver, il faut connaître les gestes et les raccourcis qui sont disponibles. En effet il existe une vingtaine de raccourcis disponibles, en variant le nombre de doigts et les gestes à effectuer sur l’écran.

Une fois que ces gestes sont maîtrisés, on peut comprendre comment les utilisateurs de VoiceOver vont utiliser notre application. Ces étapes préliminaires doivent être la priorité bien avant le développement, c’est-à-dire durant la phase de conception. Rendre une application accessible est une réflexion qui commence au tout début du projet.

La plupart du temps on ajoute le support de VoiceOver trop tard, lorsque l’application est déjà sur les stores et que l’on vise une V2 ou une V3. Dans ces cas-là l’impact est double : d’une part les choix qui ont été fait durant les phases de conception ou de design ont de grandes chances d’être incorrects pour les utilisateurs de VoiceOver. D’autre part le temps requis pour l’intégration de VoiceOver est plus long que si cela avait été pensé au début.

Il n’y a pas de formule magique pour dire combien de temps prend l’intégration de VoiceOver. Mais il faut garder en tête que cette fonctionnalité impacte chaque écran de l’application, et que l’intégration est d’autant plus difficile que l’interface utilisateur est complexe (vues inhabituelles, gestes interactifs, etc.).

Supporter VoiceOver dans une application signifie que l’on doit rendre chaque élément affiché à l’écran accessible à l’utilisateur. Quand VoiceOver est activé, le système va itérer sur toutes les sous-vues et lire la description associée à chaque vue. Le contenu qui est lu à l’utilisateur est modifiable via la property accessibilityLabel de NSObject. Les éléments qui de prime abord semblent intuitifs et simples pour des utilisateurs valides peuvent en réalité devenir très difficiles à utiliser pour ceux qui ne peuvent pas les voir. C’est pourquoi les utilisateurs de VoiceOver attendent des labels de qualité pour tous les éléments.

En plus de la property accessibilityLabel qui est la description statique de l’élément, il est possible de spécifier une aide (ou suggestion) pour l’utilisation de l’élément (via la property accessibilityHint) qui décrit les différentes actions possibles. Un texte statique n’aura pas de suggestion associée, mais on peut vouloir en rajouter à des boutons par exemple. Ces suggestions expliquent à l’utilisateur comment interagir avec l’élément courant (geste à effectuer, nombre de doigts à utiliser, résultat attendu, etc.).

La dernière fonctionnalité majeure de l’accessibilité est la caractéristique d’un élément (via la property accessibilityTraits). Les caractéristiques (UIAccessibilityTraits) aident VoiceOver à comprendre la signification et l’usage des éléments. Par défaut, UIKit applique des caractéristiques pour les vues natives mais on peut choisir d’ajouter une caractéristique particulière. Par exemple, si VoiceOver cible un élément qui a la caractéristique UIAccessibilityTraitButton, il va traiter cet élément comme un bouton, même si la vue en question ne sous-classe pas UIButton.

Ces différents éléments d’accessibilité, que ce soit les labels, les suggestions ou les caractéristiques, sont à traiter de la même manière que le contenu utilisé pour configurer les vues (couleur, taille, titre, etc), excepté qu’il sera caché pour la plupart des utilisateurs.

Dans des cas simples, l’implémentation par défaut de VoiceOver est suffisante, mais il y a des cas de bords qui requièrent des comportements différents. En voici quelques-uns.

Lire des nombres

Dans nos applications, nous avons parfois besoin d’afficher des valeurs numériques aux utilisateurs. Par exemple, imaginons que l’on affiche la population d’une ville, disons 520215.

Par défaut, si nous créons un label et ajustons sa property text à cette valeur, VoiceOver va lire la phrase suivante :

« cinq deux zéro deux un cinq »

Ce n’est pas le résultat escompté et il est impossible de matérialiser le nombre dans notre tête. Une solution simple est de créer une autre valeur qui doit être lue par VoiceOver. Dans ce cas-là, nous pouvons utiliser le style .spellOut de la classe NSNumberFormatter.

let population = 520215
let formatter = NumberFormatter()
formatter.numberStyle = .spellOut
let accessibilityLabel = formatter.string(from: NSNumber(value: population))

// Configure the label
label.text = String(population) // "520215"
label.accessibilityLabel = accessibilityLabel // "five hundred twenty thousand two hundred fifteen"

Le système va dorénavant lire la valeur que l’on a mise pour accessibilityLabel, qui est compréhensible pour un être humain :

« cinq cent vingt mille deux cent quinze »

Ajuster les valeurs

Imaginons que l’on affiche un stepper à l’écran, pour mettre à jour la valeur d’un compteur. L’interface utilisateur est la suivante :

Exemple de Stepper
Exemple de Stepper

Si on laisse les valeurs d’accessibilité par défaut, voici ce qui se passe : le focus va aller sur le bouton moins, puis sur le bouton plus, et enfin sur le label.

Focus par défaut
Focus par défaut

Voici ce qui est lu par VoiceOver durant ces actions :

  • Focus sur le bouton moins : « Décrément, estompé, bouton »
  • Focus sur le bouton plus : « Incrément, bouton »
  • Double clic sur le bouton plus : « Incrément »
  • Focus sur le label : « Compte : un point zéro »

Ce n’est pas efficace pour plusieurs raisons. Premièrement il faut sélectionner deux éléments différents pour changer la valeur (boutons plus et moins) et un troisième pour lire la valeur du compteur (le label). Deuxièmement quand on interagit avec un des boutons (plus ou moins) il n’y a pas de retour comme quoi la valeur du compteur a changé, ni la confirmation de sa nouvelle valeur.

Une solution à ce problème exige de changer l’interface utilisateur. On peut créer une vue englobante qui va contenir le stepper et le label. Ensuite il faut dire à VoiceOver de considérer cette vue englobante comme accessible, mais pas ses sous-vues. De cette manière le système ciblera seulement la vue englobante mais pas le label ou le stepper. Enfin il faut changer la caractéristique de la vue englobante à UIAccessibilityTraitAdjustable.

Comme nous l’avons vu précédemment, les caractéristiques décrivent des aspects de l’état, du comportement ou de l’usage d’un élément. Dans notre cas nous voulons que l’élément soit ajustable (UIAccessibilityTraitAdjustable). Apple marque dans sa documentation :

Utilisez cette caractéristique pour caractériser un élément d’accessibilité que les utilisateurs peuvent ajuster de manière continue, comme un slider ou un picker. Si vous spécifiez cette caractéristique sur un élément d’accessibilité, vous devez également implémenter les méthodes accessibilityIncrement() et accessibilityDecrement() du protocol UIAccessibilityAction.

Focus sur la vue englobante
Focus sur la vue englobante

Voici le résultat après la mise à jour :

  • Focus sur la vue englobante : « Compteur. Compte : zéro point zéro. Ajustable. Balayer l’écran vers le haut ou le bas avec un doigt pour ajuster la valeur »
  • Balayage vers le haut : « Compte : un point zéro »

Notons que cette fois ci toutes les informations sont lues au même endroit. Il n’y a plus besoin de changer le focus d’élément. De plus, la caractéristique UIAccessibilityTraitAdjustable fournit la suggestion (hint) par défaut :

« Ajustable. Balayer l’écran vers le haut ou le bas avec un doigt pour ajuster la valeur »

Voici le code utilisé dans cet exemple :

protocol AccessibilityAdjustableContainerViewDelegate : class {
    func accessibilityAdjustableContainerViewDidIncrement(_ view: AccessibilityAdjustableContainerView)
    func accessibilityAdjustableContainerViewDidDecrement(_ view: AccessibilityAdjustableContainerView)
}

class AccessibilityAdjustableContainerView : UIView {

    weak var delegate: AccessibilityAdjustableContainerViewDelegate?

    override var accessibilityTraits: UIAccessibilityTraits {
        get {
            return super.accessibilityTraits | UIAccessibilityTraitAdjustable
        }
        set { super.accessibilityTraits = newValue }
    }

    override var isAccessibilityElement: Bool {
        get {
            return true
        }
        set { super.isAccessibilityElement = newValue }
    }

    override func accessibilityIncrement() {
        delegate?.accessibilityAdjustableContainerViewDidIncrement(self)
    }

    override func accessibilityDecrement() {
        delegate?.accessibilityAdjustableContainerViewDidDecrement(self)
    }
}
class ViewController: UIViewController, AccessibilityAdjustableContainerViewDelegate {

    @IBOutlet var stepper: UIStepper!
    @IBOutlet var label: UILabel!
    @IBOutlet var wrapperView: AccessibilityAdjustableContainerView!

    override func viewDidLoad() {
        super.viewDidLoad()
        wrapperView.delegate = self
        wrapperView.accessibilityLabel = "Compteur"
        stepper.value = 0
        updateLabel()
    }

    //MARK: - AccessibilityAdjustableContainerViewDelegate

    func accessibilityAdjustableContainerViewDidDecrement(_ view: AccessibilityAdjustableContainerView) {
        let newValue = stepper.value - 1
        guard newValue >= stepper.minimumValue else { return }
        stepper.value = newValue
        updateLabel()
    }

    func accessibilityAdjustableContainerViewDidIncrement(_ view: AccessibilityAdjustableContainerView) {
        let newValue = stepper.value + 1
        guard newValue <= stepper.maximumValue else { return }
        stepper.value = newValue
        updateLabel()
    }

    //MARK: - Private

    @IBAction private func stepperValueChanged(_ sender: UIStepper) {
        updateLabel()
    }

    private func updateLabel() {
        label.text = "Compte : \(stepper.value)"
        wrapperView.accessibilityValue = label.text
    }
}

Dans le code précédent on met à jour la valeur wrapperView.accessibilityValue à chaque fois que le label est actualisé. C’est parce que le système va lire cette valeur seulement lorsqu’elle change.

Le rotor

On peut afficher le rotor en appuyant sur l’écran avec deux doigts et en les faisant tourner vers la droite ou vers la gauche.

Le rotor avec VoiceOver
Le rotor avec VoiceOver

Le rotor affiche plusieurs actions ou mouvements qui peuvent être effectués:

  • Conteneurs (pour se déplacer de conteneur en conteneur)
  • En-têtes (pour se déplacer d’en-tête à en-tête)
  • Ajuster la valeur (si l’élément est UIAccessibilityTraitAdjustable)
  • Personnalisé (depuis iOS 10 avec UIAccessbilityCustomRotor)
  • Actions (depuis iOS 8 avec UIAccessbilityCustomAction)
  • Caractères (lit les mots caractère par caractère)
  • Mots (lit le contenu mot par mot)
  • Débit vocal (ajuste le débit vocal)

Une fois que la valeur du rotor est choisie, il est possible de bouger d’un élément à un autre en balayant avec un doigt vers le haut ou le bas.

Par exemple, si on sélectionne la valeur En-têtes, on pourra se déplacer entre les éléments qui ont la caractéristique UIAccessibilityTraitHeader.
Dans l’exemple ci-dessous, si le focus est sur le premier en-tête En tête 1 et que la valeur En-têtes est sélectionnée sur le rotor, quand on balaye l’écran avec un doigt vers le bas, le focus va cibler le second en-tête En tête 2, en évitant le contenu entre les deux.

Change le focus du premier en-tête au second
Change le focus du premier en-tête au second

Cette fonctionnalité permet aux utilisateurs de VoiceOver de gagner du temps et de pouvoir scanner uniquement les éléments pertinents. C’est pourquoi il est crucial de bien choisir la caractéristique de chaque élément.

Depuis iOS 8, Apple a décidé de permettre la personnalisation du rotor, d’abord avec l’API de UIAccessbilityCustomAction puis avec UIAccessbilityCustomRotor à partir d’iOS 10.

Un objet du type UIAccessbilityCustomAction est simplement une paire de target/action qui est nommée et qui permet de créer une action personnalisée pour un élément graphique. Le nom associé est affiché dans le rotor.

Si l’on veut définir un mouvement personnalisé (comme vu juste avant pour la valeur En-têtes) on peut désormais utiliser UIAccessbilityCustomRotor. Depuis un élément source (l’élément qui a le focus par VoiceOver) et une direction, il est possible de choisir quel est le prochain élément à cibler.
Voici un exemple de cas pratique extrait de la documentation de la classe UIAccessbilityCustomRotor :

… dans une application de magazine, un rotor personnalisé peut être créé pour permettre à l’utilisateur de trouver le lien ou le titre suivant dans un article.

Un dernier point sur la valeur du rotor Conteneurs. Quand cette valeur est sélectionnée, on peut changer de conteneur, comme par exemple UINavigationController ou UITabBarController. Cela permet d’accéder facilement à la navigationBar ou à la tabBar lorsque VoiceOver cible un élément ailleurs sur l’écran.

Malheureusement il n’est pas encore possible de définir nos propres conteneurs. L’API est privée et Apple utilise une valeur spéciale de UIAccessibilityTraits pour avoir ce comportement.
Si on inspecte une instance de UINavigationBar, on peut voir la constante utilisée, qui n’est documentée nulle part :

po [navigationBar accessibilityTraits] => 0x0000200000000000

Si l’on veut séparer notre interface utilisateur en plusieurs sections logiques, il faut alors se rabattre sur la caractéristique UIAccessibilityTraitHeader à la place.

Les notifications

L’utilisateur doit pouvoir rester informé des changements à l’écran lorsque les éléments changent de place ou se mettent à jour sans action spécifique de sa part. Pour cela on peut forcer VoiceOver à lire des annonces grâce à des notifications.

La méthode UIAccessibilityPostNotification() est faite pour cela. Il est possible, en plus de la notification, de passer des paramètres optionnels qui permettent de cibler un élément en particulier.

On utilise cette fonctionnalité lorsqu’on veut dire à VoiceOver de cibler un élément en particulier. La plupart du temps le problème apparaît lorsqu’un écran disparaît et que c’est le mauvais élément qui est lu par VoiceOver lorsqu’on revient sur l’écran précédent.
Nous avons essayé d’utiliser les deux notifications UIAccessibilityScreenChangedNotification et UIAccessibilityLayoutChangedNotification pour forcer VoiceOver à cibler un élément. Mais les résultats ne sont pas toujours cohérents, parfois les deux notifications fonctionnent, parfois une seule des deux. VoiceOver est une boîte noire et il est difficile de savoir comment l’ordre de lecture des éléments est choisi. C’est pourquoi vouloir forcer le système ne fonctionne pas à tous les coups.

Réagir à VoiceOver

On peut tester si VoiceOver est activé de deux manières différentes :

  • de manière pro-active avec la méthode UIAccessibilityIsVoiceOverRunning()
  • de manière réactive en écoutant la notification UIAccessibilityVoiceOverStatusChanged

On peut utiliser ces méthodes pour mettre à jour l’interface utilisateur en fonction de l’activation de VoiceOver.
Par exemple imaginons que nous avons un bouton dropdown et que nous voulons afficher un picker comme inputView de ce bouton lorsque l’on clique dessus (image ci-dessous).

Comportement par défaut quand le bouton dropdown est sélectionné
Comportement par défaut quand le bouton dropdown est sélectionné

Une solution serait de laisser l’utilisateur choisir une valeur dans la liste du picker que VoiceOver soit activé ou non. Mais utiliser un UIPickerView avec VoiceOver est très difficile.

Une meilleure solution serait de laisser l’utilisateur sélectionner une valeur dans le picker si VoiceOver n’est pas activé (comme pour les utilisateurs classiques). On pourrait alors utiliser la caractéristique UIAccessiblityTraitAdjustable pour laisser l’utilisateur choisir la valeur en balayant vers le haut ou le bas avec un doigt, et ainsi éviter d’afficher le picker si VoiceOver est activé.

Comportement avec VoiceOver
Comportement avec VoiceOver

Conclusion

L’accessibilité sur iOS est une fonctionnalité puissante qu’Apple a toujours mis au coeur de ses produits depuis le début (par exemple, même l’application Appareil Photo est accessible).

Il est important d’intégrer le support de l’accessibilité dans votre application si elle va être utilisée par des personnes avec des invalidités visuelles (ce qui est toujours le cas si vous ciblez un large panel d’utilisateurs) car elle pourra toucher plus d’utilisateurs. De plus l’accessibilité peut améliorer l’expérience de tous les utilisateurs : par exemple, la fonctionnalité d’auto-complétion a d’abord été développée comme une fonctionnalité d’accessibilité avant d’être utilisée par tout le monde.

Par ailleurs il est important de penser l’accessibilité d’une application dès le début de la conception pour être sûr que les choix qui sont faits ciblent bien tous les utilisateurs. Les implémentations par défaut d’Apple ont le méritent d’exister mais il est souvent nécessaire de les personnaliser pour améliorer l’expérience finale.

Si vous voulez discuter plus en détail sur le sujet, n’hésitez pas à nous contacter sur Twitter!

Nous sommes toujours à la recherche de personnes compétentes.

Rejoignez-nous!
logo business unit

FABERNOVEL TECHNOLOGIES

150 talents pour répondre aux défis technologiques de la transformation digitale.

à lire