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
flowchart LR
VC["ViewController"] --> Scene["ARSKView / Scene"]
VC --> Conversation
Script["conversation.txt"] --> Reader["ScriptReader"]
Reader --> Quotes["[Quote]"]
Quotes --> Conversation["Conversation"]
Conversation --> Synth["AVSpeechSynthesizer"]
Synth --> Conversation
Conversation --> Bus["NotificationCenter"]
Bus --> VC
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
classDiagram
ViewController *-- Conversation
ViewController o-- ARSKView : outlet
Conversation *-- AVSpeechSynthesizer
Conversation *-- Quote : parsed list
Conversation ..|> AVSpeechSynthesizerDelegate
Conversation --> NotificationCenter : publishes
ViewController --> NotificationCenter : observes
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...Dataprefix, 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 |