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

WWDC21 Challenge: Speech Synthesizer Simulator

At a glance

Item Summary
Purpose Place two AR/SpriteKit characters and animate them while AVSpeechSynthesizer reads a script with accessible captions.
Architecture One UIKit controller owns the scene and a Conversation speech coordinator; notifications carry current speaker/line changes back to the UI.
Main pattern Conversation acts as a narrow view model around AVFoundation and its delegate callbacks.
Boundary Code-light single app target; no extension, persistence, or custom protocol abstraction.

Project structure

Speech Synthesizer Simulator/
├── ViewController.swift        # UI, AR scene, animation
├── Conversation.swift          # speech queue and delegate
├── ScriptReader.swift          # text-to-Quote parsing
├── Scene.swift                 # AR anchor placement
├── DialogueLabel.swift
└── SpriteHelper.swift

The split is by framework responsibility, not by formal layers.

Overall architecture

Reference code

Speech Synthesizer Simulator/Conversation.swift:22 — the speech coordinator owns parsing and the framework delegate (abridged).

override init() {
    super.init()
    synthesizer.delegate = self
    quotes = ScriptReader().parseFile()
}

let synthesizer = AVSpeechSynthesizer()

Ownership and state

Ownership evidence

Speech Synthesizer Simulator/ViewController.swift:38 — the screen retains both script and speech helpers alongside presentation state.

var scriptReader = ScriptReader()
// Conversation abstracts AVSpeechSynthesizer details.
var conversation = Conversation()

NotificationCenter.default.addObserver(
    self,
    selector: #selector(updateLine(_:)),
    name: Notification.Name("AVLineData"),
    object: nil
)

Conversation is the speech mutation authority; the controller owns scene/animation state. NotificationCenter.default is shared infrastructure, not owned by either object.

Class and protocol design

Conversation: NSObject, AVSpeechSynthesizerDelegate isolates the framework protocol. ScriptReader and Quote are value types with deterministic parsing. Scene: SKScene owns anchor placement. No app-defined protocol is present; the sample uses framework inheritance and named notifications directly.

Access control

All declarations use Swift’s internal default; the source contains no explicit private, fileprivate, or public declarations. That keeps a short challenge easy to modify, but it means ownership is conventional rather than compiler-enforced. A production version could make synthesizer state and parser helpers private without changing collaboration.

Logic ownership and placement

Logic Owner Placement reason
Script decoding and fallback dialogue ScriptReader Parsing remains independent of speech/UI (Speech Synthesizer Simulator/ScriptReader.swift:20).
Utterance configuration and queueing Conversation One owner controls AVSpeechSynthesizer (Speech Synthesizer Simulator/Conversation.swift:40).
Speaker/caption rendering and animation ViewController It owns labels, nodes, and play/reset actions.
Anchor creation Scene Touch input and AR session transform are local (Speech Synthesizer Simulator/Scene.swift:11).

Design patterns

Pattern Evidence Purpose / tradeoff
View-model-like coordinator Conversation hides AVFoundation details Keeps the controller readable without defining a protocol.
Observer Speaker and line notifications Decouples speech callbacks from UI, but string names are not type-safe.
Framework delegate Synthesizer didStart callback Advances speaker/line state at the framework lifecycle boundary.
Parser ScriptReader returns [Quote] Keeps external text format out of the speech object.

Naming conventions

  • Framework coordinators use role nouns (Conversation, ScriptReader, Scene).
  • UI callbacks use action/update verbs (pressPlay, updateSpeaker, updateLine).
  • Notification names use the AV...Data prefix, although typed static names would be safer at larger scale.

Architecture takeaways

  • Wrap a stateful framework object in one owner that adopts its delegate.
  • Parse external script data before it enters playback logic.
  • Keep scene presentation and speech sequencing separate even in a small sample.
  • The intentionally broad internal access reflects a challenge scaffold, not a recommended public API surface.

Source map

Source Role
Speech Synthesizer Simulator/AppDelegate.swift:10 App entry point
Speech Synthesizer Simulator/ViewController.swift:53 UI and observer setup
Speech Synthesizer Simulator/ViewController.swift:110 Playback action routing
Speech Synthesizer Simulator/Conversation.swift:32 Speech queue
Speech Synthesizer Simulator/Conversation.swift:77 Delegate-to-notification bridge
Speech Synthesizer Simulator/ScriptReader.swift:39 Script parsing