Sample CodeiOS 15.0+, iPadOS 15.0+, Mac Catalyst 15.0+Reviewed 2026-07-21View on Apple Developer

WWDC21 Challenge: VoiceOver Maze

At a glance

Item Summary
Purpose Build a VoiceOver-first branching maze using virtual accessibility elements instead of visible maze controls.
Architecture An instruction controller presents a maze controller; the maze derives stage text and decisions from lookup tables.
State strategy The current stage is implicit in each generated button element’s nextStage; activation rebuilds the accessibility tree.
Boundary Code-light single UIKit target with no persistence, extension, or service layer.

Project structure

WWDC_VoiceOverMaze/
├── ViewController.swift
├── MazeViewController.swift
├── VoiceOverActivatableButton.swift
├── AppDelegate.swift
└── SceneDelegate.swift

Most of the app’s behavior is deliberately concentrated in MazeViewController for the challenge.

Overall architecture

Reference code

WWDC_VoiceOverMaze/MazeViewController.swift:245 — a stage is rendered as a new virtual accessibility tree (abridged).

private func initializeStage(_ stage: Int) {
    let instructionString = self.instructionString(stage)
    let buttons = self.buttonInfo(stage)
    var elements = [UIAccessibilityElement]()

    for buttonInfo in buttons {
        let element = ButtonAccessibilityElement(accessibilityContainer: self)
        element.buttonInfo = buttonInfo
        element.mazeController = self
        elements.append(element)
    }
    view.accessibilityElements = elements
}

Ownership and state

Ownership evidence

WWDC_VoiceOverMaze/MazeViewController.swift:39 — virtual buttons retain decision data but only weakly reference their coordinator.

class ButtonAccessibilityElement: UIAccessibilityElement {
    var buttonInfo: ButtonInfo?
    weak var mazeController: MazeViewController?

    override func accessibilityActivate() -> Bool {
        mazeController?.initializeStage(buttonInfo.nextStage)
        return true
    }
}

The controller replaces view.accessibilityElements for every stage. The weak back-reference prevents those child elements from retaining the controller. The intro button’s command closure captures its controller directly; the sample does not add a weak capture.

Class and protocol design

The design subclasses framework types instead of defining protocols: VoiceOverActivatableButton: UIButton translates VoiceOver activation into a closure, while nested ButtonAccessibilityElement translates activation into a stage transition. ButtonInfo is a value command containing a decision and destination.

Access control

Symbol Access Rationale
setup() private Only the intro controller’s initializers assemble its views (WWDC_VoiceOverMaze/ViewController.swift:27).
Stage lookup and rendering helpers private Only MazeViewController may interpret/rebuild maze state (WWDC_VoiceOverMaze/MazeViewController.swift:57).
ButtonInfo and custom control types internal default They collaborate across the small app target.
Public/file-private API none There is no module boundary or same-file extension requirement.

Logic ownership and placement

Logic Owner Placement reason
Intro layout and presentation ViewController It owns the start-screen controls.
Stage graph and transition rendering MazeViewController Text, decisions, and accessibility frames change atomically.
Generic activation callback VoiceOverActivatableButton Converts a UIKit hook into a reusable command (WWDC_VoiceOverMaze/VoiceOverActivatableButton.swift:10).
Winning screen MazeViewController Terminal stage shares the maze’s presentation lifecycle.

Design patterns

Pattern Evidence Purpose
Table-driven state machine Stage-indexed text and decisions Expresses branching without many screen classes.
Command object/value ButtonInfo Carries transition intent into generated elements.
Custom accessibility adapter Virtual element overrides activation Makes nonvisual navigation the primary interaction.
Command closure Start button’s activatedHandler Avoids a one-method delegate for one action.

Naming conventions

  • ButtonInfo.Decision names domain choices; nextStage names transition data.
  • UIKit subclasses carry explicit role suffixes (ViewController, Button, AccessibilityElement).
  • Builder methods start with configure or initialize; lookup helpers use domain nouns.

Architecture takeaways

  • A stage graph can generate the accessibility hierarchy as derived UI.
  • Keep child-to-owner references weak when the owner retains the children.
  • Use value commands for transitions and a controller for atomic tree replacement.
  • This sample is intentionally VoiceOver-first and compact; it is not a general game engine.

Source map

Source Role
WWDC_VoiceOverMaze/AppDelegate.swift:10 App entry point
WWDC_VoiceOverMaze/ViewController.swift:50 Start command wiring
WWDC_VoiceOverMaze/MazeViewController.swift:10 Decision value
WWDC_VoiceOverMaze/MazeViewController.swift:125 Maze lifecycle
WWDC_VoiceOverMaze/MazeViewController.swift:257 Virtual element creation
WWDC_VoiceOverMaze/VoiceOverActivatableButton.swift:14 VoiceOver command hook