Mastering App Development and UIAccessibility with Swift: Adventure application

0 of 13 lessons complete (0%)

Intro & Game End screens

Intro & Game End screens

The iOS Accessibility Handbook

The first step is to onboard the user to the game concept, we will use the same module for both the intro and game end screens as we simply want ; we will only change the data set and provide a “Replay” button for the game end screen to reset the game to its initial state.
As always we will only highlight the interesting points here, please refer to the GitHub repository for any additional info (XIB files, AppDelegate, resource files…).

The iOS Accessibility Handbook

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

notification: .layoutChanged,

argument: self.instructionsButton )

startInstructionsUpdateTimer() }

override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated) .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 {


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

}) }


The iOS Accessibility Handbook

// 1: Here we will use the Coordinator pattern to act as a router between our different screens.
// 2: We define a Scenario enum to define which script we will load (Intro of Game End).
// 3: We store the script instructions in a dedicated collaborator class: InstructionsInteractor. We store the dependency as a protocol to be resilient to change: if we decide later to fetch scripts from a database data source, performing the change will be as simple as modifying the interface to send the result as a callback and swapping the implementation.
// 4: We fetch the correct instructions based on scenario.

The iOS Accessibility Handbook

// 5: This is the first interesting accessibility mechanism: we post a layout changed notification using under the hood: UIAccessibility.Notification, argument: Any?).

You should definitely take a look at the documentation for the different available notifications.

Here we use the UIAccessibility.Notification.layoutChanged notification to notify VoiceOver that something interesting has changed on the instructionsButton view and that it should focus it and describe it. It is a good practice to choose the focussed element on each screen being presented in viewDidAppear(_:).

Another useful notification to post is: .announcement, argument: “Hello world!”)

That would cause the text passed as argument to be read by the VoiceOver engine.

// 6: if it is intro screen, we route to the first game chapter. If it is game end screen, we simply reset the game.

The remaining code is simple view logic using Timer, we display the next instruction while there is one.

And the collaborator class that will simply return our scripts for both screens:

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 [

“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 :)”

] }