WWDC22 Challenge: Learn Switch Control through gaming
At a glance
| Item | Summary |
|---|---|
| Purpose | Teach Switch Control through a memory-matching game whose cards act through accessibility actions. |
| Architecture | A code-light SwiftUI app where GameBoardView owns an observable Game; Game mutates value-type Deck and Card models. |
| Main pattern | View-model ownership with explicit delayed match/mismatch transitions and accessibility announcements. |
| Boundary | One app target; no extension, persistence, network layer, or custom protocol. |
Project structure
SwitchControlChallenge/
├── Models/ # Game, Deck, Card, Symbol
├── Views/
│ ├── GameBoardView.swift
│ ├── CardView.swift
│ └── Intro/
└── SwitchControlChallengeApp.swift
The folder split cleanly separates game mutation from declarative rendering.
Overall architecture
flowchart LR
App["SwitchControlChallengeApp"] --> Content["ContentView"]
Content --> Board["GameBoardView"]
Board --> Game["Game ObservableObject"]
Game --> Deck["Deck value"]
Deck --> Card["Card values"]
Board --> CardView["CardView"]
CardView --> AX["Accessibility action"]
AX --> Game
Reference code
SwitchControlChallenge/Views/GameBoardView.swift:23 — interaction is routed through accessibility, not tap handling (abridged).
ForEach(viewModel.cards[range]) { card in
CardView(card: card)
.onTapGesture {
// Only playable via Switch Control!
}
.accessibilityAction {
viewModel.select(card)
}
}Ownership and state
classDiagram
GameBoardView *-- Game : StateObject
Game *-- Deck
Deck *-- Card
Game *-- Int : score
Game *-- Int : selected index
CardView o-- Card : value input
Ownership evidence
SwitchControlChallenge/Models/Game.swift:11 — Game is the single mutation authority (abridged).
class Game: ObservableObject {
@Published private(set) var deck: Deck
@Published var score = 0
@Published var didWinGame = false
private var currentlySelectedCardIndex: Int?
func select(_ card: Card) {
deck.cards[requestedCardIndex].isFaceUp = true
// resolve match or mismatch
}
}GameBoardView owns the Game lifecycle through @StateObject; CardView receives snapshots and sends actions back to the owner.
Class and protocol design
Game adopts the framework ObservableObject protocol. Deck and Card are mutable value types; Card adopts Identifiable for SwiftUI diffing. There is no app-defined protocol because the small view hierarchy depends directly on one game behavior surface (select and reset).
Access control
| Symbol | Access | Rationale |
|---|---|---|
deck setter |
private(set) |
Views can render cards but only Game can replace the deck (SwitchControlChallenge/Models/Game.swift:13). |
| Selected index and derived counts | private |
They are game-rule implementation details. |
score, didWinGame |
internal mutable | The view observes them; the sample does not enforce read-only access. |
| Models/views | internal default | Single-target teaching app, with no public surface. |
No public or fileprivate boundary is needed. The strongest guard is on the deck’s setter because replacement must remain coordinated.
Logic ownership and placement
| Logic | Owner | Placement reason |
|---|---|---|
| Match selection, score, delayed flips | Game |
All rules mutate the same published state (SwitchControlChallenge/Models/Game.swift:29). |
| Pair construction and shuffle | Deck |
Value-level collection operations stay with the collection (SwitchControlChallenge/Models/Deck.swift:14). |
| Accessibility label | Card |
It derives only from card state (SwitchControlChallenge/Models/Card.swift:20). |
| Grid and presentation state | GameBoardView |
Sheets/popovers and layout are screen concerns. |
Design patterns
| Pattern | Evidence | Purpose |
|---|---|---|
| MVVM-like split | GameBoardView owns Game |
Rules stay out of SwiftUI body code. |
| Observable state | Published deck/score/win state | Drives automatic rendering. |
| Value model | Deck and cards are structs | Simple local mutation and identity snapshots. |
| Accessibility command | .accessibilityAction invokes select |
Makes Switch Control the intended input path. |
Naming conventions
- Domain nouns are short and singular (
Game,Deck,Card,Symbol). - Boolean state uses
is.../did...(isFaceUp,isMatched,didWinGame). - View types mirror their domain (
GameBoardView,CardView).
Architecture takeaways
- Let the
StateObjectowner control the lifetime of an observable game model. - Expose value snapshots to views and route mutations back through commands.
- Put derived accessibility labels on the model when they depend only on model state.
- The one-second tasks are game feedback timing, not a general scheduler abstraction.
Source map
| Source | Role |
|---|---|
SwitchControlChallenge/SwitchControlChallengeApp.swift:10 |
App entry point |
SwitchControlChallenge/Views/GameBoardView.swift:14 |
Game owner |
SwitchControlChallenge/Views/CardView.swift:28 |
Card accessibility semantics |
SwitchControlChallenge/Models/Game.swift:42 |
Delayed match transition |
SwitchControlChallenge/Models/Deck.swift:28 |
Shuffle and index mutation |
SwitchControlChallenge/Models/Card.swift:10 |
Identifiable card value |