Jour 3-6 - Développement

Nous y sommes, vous avez défini vos spécifications pour notre application, le design est prêt, il est maintenant temps de l'implémenter.

Zeplin

Pour vous aider à concevoir vos écrans de manière programmatique, vous pouvez exporter vos esquisses vers Zeplin.
Cela peut vous faire gagner beaucoup de temps car tous les écrans, couleurs et polices sont facilement accessibles.
Le point clé que nous aimons dans Zeplin est qu'il est très facile de voir les contraintes que vous devez mettre en œuvre dans l'application iOS car les marges en pixels entre chaque élément sont clairement affichées lorsque l'on survole l'élément en question.

Le principe SOLID

Pour respecter des délais courts, vous devez avoir une connaissance approfondie des principes de programmation, de l'architecture logicielle et des stratégies de test.

Vous devriez essayer de réduire autant que possible le couplage entre votre vue et votre logique, et extraire vos opérations de mise en réseau dans un bloc séparé. Gardez également à l'esprit que vous devez concevoir votre module de jeu de manière à ce qu'il soit réutilisable autant que possible afin d'améliorer votre productivité lorsque vous démarrez un nouveau projet.

Le principe SOLID est un concept clé de la programmation orientée objet qui résume ces idées :

S : Single responsibility principle : chaque classe est responsable d'une et d'une seule tâche (ainsi, aucune interface utilisateur ne doit être affichée par votre classe d'interface réseau).
O : Open/closed principle : chaque module que vous développez doit être ouvert à l'extension mais fermé à la modification, c'est-à-dire que l'architecture doit anticiper les changements ultérieurs et donc fournir des moyens d'ajouter de nouveaux éléments connexes de manière simple par le biais de l'héritage, par exemple.
L : Liskov substitution principle : une sous-classe doit préserver l'objectif de sa classe mère,  Les méthodes ou propriétés surchargées doivent avoir la même signification que dans la classe mère.
I : Interface segregation principle : l'interface est définie par de petits rôles uniques, une classe doit pouvoir l'implémenter et, le cas échéant, n'implémenter que les méthodes qui lui sont utiles.
D : Dependancy inversion principle : les modules de haut niveau ne doivent pas dépendre des modules de bas niveau, vous devez introduire une sorte d'abstraction par le biais d'une interface. Une excellente explication peut être trouvée ici : https://www.oodesign.com/dependency-inversion-principle.html

Vous pouvez en découvrir plus sur la Clean Architecture dans cette formation Clean Architecture.

Il s'agit de principes fondamentaux qui peuvent être appliqués à tout type de projet logiciel.

Nous appliquerons ces concepts de programmation à un exemple concret : "Escape From Blindness", une application de type Escape Room accessible aux utilisateurs déficients visuels.

Exemple de développement d'application : Escape From Blindness

Le principe de l'application est simple : vous devrez franchir différents niveaux en résolvant des problèmes liés à l'utilisation de VoiceOver, l'outil principal d'accessibilité pour les utilisateurs déficients visuels sur la plateforme iOS.

Il s'agit d'une application simple conçue pour illustrer les concepts fondamentaux de la bibliothèque UIAccessibility.

Le jeu comporte quatre modes de jeu :

  1. Un quiz avec des questions fermées : vous passez au niveau suivant si vous sélectionnez la bonne réponse.
  2. Un quiz avec des questions ouvertes : vous passez au niveau suivant si vous tapez la bonne réponse dans le champ de texte prévu à cet effet.
  3. Niveaux basés sur le Rotor d'accessibilité iOS : nous utiliserons le rotor VoiceOver pour valider les réponses et passer au niveau suivant.
  4. Actions personnalisées basées sur les niveaux : nous utiliserons UIAccessibilityCustomAction pour valider les réponses, vous devez sélectionner valider toutes les réponses du niveau dans n'importe quel ordre pour valider le niveau.

L'ensemble du jeu ne sera utilisable que si le lecteur d'écran iOS VoiceOver est activé ; si ce n'est pas le cas, nous indiquerons à l'utilisateur comment l'activer.

Nous développerons l'application dans l'ordre suivant :

  • Définir les écrans d'introduction et de fin de jeu expliquant le concept du jeu et remerciant l'utilisateur d'avoir joué.
  • Définir l'écran d'explication de VoiceOver chargé d'expliquer que VoiceOver est obligatoire pour utiliser ce jeu, et d'expliquer comment l'activer.
  • Définir le GameFlow chargé de valider les réponses et de déléguer le routage du niveau suivant / du chapitre suivant au composant AppCoordinator.
  • Définir l'AppCoordinator responsable de l'enchaînement
    en fonction du scénario des chapitres
  • Développer les niveaux de type Closed Questions
  • Développer les niveaux de type Open Questions
  • Développer les niveaux basés sur l'utilisation du Rotor d'accessibilité
  • Développer les niveaux basés sur les Custom Actions du rotor d'accessibilité

Vous pouvez voir le code source du projet ici.

Dans cet article, nous ne parlerons que des écrans d'introduction et de fin de partie.

Nous utiliserons le même module pour les écrans d'introduction et de fin de jeu, car nous voulons simplement montrer des textes de manière séquentielle à l'utilisateur ; nous ne changerons que l'ensemble des données et nous fournirons un bouton "Replay" pour l'écran de fin de jeu afin de réinitialiser le jeu à son état initial.
Comme toujours, nous ne soulignerons ici que les points intéressants, veuillez vous référer au dépôt GitHub pour toute information complémentaire (fichiers XIB, AppDelegate, fichiers de ressources...).

class InstructionsViewController: UIViewController, Coordinated {
    var coordinator: AppCoordinatorProtocol? // 1
    enum Scenario {
        case intro
        case gameEnd 
    }
    var scenario: Scenario! // 2
    var interactor: InstructionsInteractorProtocol? = InstructionsInteractor() // 3
    private var instructions: [String] = []
    private var currentInstructionIndex = 0 
    private var currentInstruction: String? {
        return self.instructions[safe: self.currentInstructionIndex]
    }

    @IBOutlet weak var instructionsButton: UIButton!
    var timer: Timer?

    override func viewDidLoad() {
        super.viewDidLoad()
        guard let instructions = scenario == .intro ? interactor?.fetchIntroInstructions() : interactor?.fetchGameEndInstructions() else { // 4 return }
        self.instructions = instructions
        if let firstInstruction = instructions.first {
            self.replayButton.setTitle(firstInstruction, for: .normal)
        }

      // 5 
      EscapeFromBlindnessAccessibility.shared.post(
          notification: .layoutChanged,
          argument: self.instructionsButton )
          startInstructionsUpdateTimer() 
      }

    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        EscapeFromBlindnessAccessibility.shared.post(notification: .layoutChanged, argument: self.instructionsButton)
    }

    override func viewWillDisappear(_ animated: Bool) { 
       super.viewWillDisappear(animated) 
        stopInstructionsUpdateTimer()
    }

    private func startInstructionsUpdateTimer() {
        timer = Timer.scheduledTimer(withTimeInterval: 5.0, repeats: true) { [weak self] timer in
            guard let strongSelf = self else { return }
            strongSelf.currentInstructionIndex += 1
            strongSelf.handleInstruction(strongSelf.currentInstruction)
        }
        timer?.fire() 
    }

    private func stopInstructionsUpdateTimer() {
        timer?.invalidate()
    }

    private func handleInstruction(_ nextInstruction: String?) { 
        if let nextInstruction = nextInstruction {
            setInstructionsText(nextInstruction) } else {
                timer?.invalidate()

    // 6
    if self.scenario == .intro {
        coordinator?.validateChapter() } else {
        self.replayButton.isHidden = false 
        }
      }
    }

    @IBOutlet weak var replayButton: UIButton!

    @IBAction func onReplayButtonTouched(_ sender: Any) {
        coordinator?.replay() 
    }

    private func setInstructionsText(_ text: String) {
        let duration = 0.5
        UIView.animate(withDuration: duration, animations: {
            self.instructionsButton.alpha = 0 }, completion: { _ in
            self.instructionsButton.setTitle(text, for: .normal) UIView.animate(withDuration: duration) {
            self.instructionsButton.alpha = 1 }
        }) }
    }
}

// 1: Ici nous utiliserons le Coordinator pattern pour agir comme routeur vers nos différents écrans.

// 2 : Nous définissons une enum Scenario pour définir quel script nous allons charger (Intro ou Game End).

// 3 : Nous stockons les instructions de script dans une classe de collaborateur dédiée : InstructionsInteractor. Nous stockons la dépendance sous la forme d'un protocole afin de résister aux changements : si nous décidons ultérieurement d'extraire des scripts d'une source de données, il suffira de modifier l'interface pour envoyer le résultat sous la forme d'un rappel et d'intervertir l'implémentation.

// 4 : Nous recherchons les instructions correctes en fonction du scénario.

// 5 : Voici le premier mécanisme d'accessibilité intéressant : nous envoyons une notification de modification de la mise en page en utilisant : UIAccessibility.post(notification : UIAccessibility.Notification, argument : Any ?).
Pour bien comprendre cette partie, vous deviez vraiment jeter un coup d'oeil à la documentation pour avoir connaissance de tous les types de notifications.
Ici, nous utilisons la notification UIAccessibility.Notification.layoutChanged pour notifier à VoiceOver que quelque chose d'intéressant a changé dans la vue instructionsButton et qu'il doit la focaliser et la décrire. Une bonne pratique consiste à choisir l'élément focalisé sur chaque écran présenté dans viewDidAppear(_ :).
Une autre notification utile à publier est la suivante :
UIAccessibility.post(notification: .announcement, argument: someString)
Le texte transmis en tant qu'argument serait alors lu par le moteur VoiceOver.

// 6 : s'il s'agit d'un écran d'introduction, nous passons au premier chapitre du jeu. S'il s'agit d'un écran de fin de jeu, nous réinitialisons simplement le jeu.

Le reste du code est une simple logique de visualisation utilisant le Timer, nous affichons la prochaine instruction tant qu'il y en a une.
Et la classe collaborateur qui renverra simplement nos instructions pour les deux écrans ; nous nous appuyons sur une abstraction (protocole) pour nous permettre d'échanger facilement les implémentations plus tard (il pourrait être récupéré à partir d'une base de données, de la mémoire locale ou autre, ce n'est qu'un détail d'implémentation) :

protocol InstructionsInteractorProtocol {
    func fetchIntroInstructions() -> [String]
    func fetchGameEndInstructions() -> [String]
}
class InstructionsInteractor: InstructionsInteractorProtocol { 
    func fetchIntroInstructions() -> [String] {
        return [
            "Welcome to Escape From Blindness!",
            "You are an explorer of an old tomb!",
            "Your torch just turned off!",
            "This game is 100% based on VoiceOver usage!",
            "VoiceOver is an assistive technology that allows blind users to use their smartphones.", 
            "In this adventure, you will have to activate VoiceOver to progress!",
            "You will be faced dozens of unique levels to will use the assistive technology's features!", 
            "Good luck explorer!"
        ] 
    }

    func fetchGameEndInstructions() -> [String] { 
        return [
            "Congratulations!",
            "You successfully managed to go through all game levels!",
            "We hope you enjoyed this game as much as we enjoyed developing it!",
            "We also hope that this game helped you to understand better the challenges encoutered by
blind people while using smartphones :)"
        ] 
    }
}

Pour obtenir une description plus détaillée de ce processus de développement d'applications et savoir comment rendre votre application plus accessible, vous pouvez consulter le projet iOS Accessibility Toolbox .

Ci-dessous une vidéo de revue du projet final:

La suite ?

Maintenant que nous avons développé une application complète, il nous reste encore du travail à faire avant de la publier sur l'App Store.

C'est l'objet de la prochaine étape de ce tutoriel !
Day 7 – Release.

En savoir plus: