A self-contained macOS app that demonstrates Page Object unit tests with a State Machine architecture, including a view data representation (ViewRep): https://davepoirier.medium.com/viewrep-intent-statemachine-ios-architecture-4e3a2d589b36).
The app lets you maintain a list of movies you want to watch, mark them as watched, search a (mock) catalogue, and persists everything to disk.
| Layer | File(s) | What it demonstrates |
|---|---|---|
| State | Sources/DomainLogic/State/* |
PersistentState (on-disk) and EphemeralState (in-memory) carried inside an actor. |
| Intent | Sources/DomainLogic/Intents/Intent.*.swift |
Atomic, serial state mutations. Each intent has a single mutate(...) method. |
| Activity | Sources/DomainLogic/Activities/Activity.*.swift |
Long-running async work that reads state via intents and writes results via intents. |
| StateMachine | Sources/DomainLogic/StateMachine/StateMachine.swift |
Single source of truth. Processes intents serially, publishes throttled ViewReps. |
| ViewRep | Sources/DomainLogic/ViewRep/* |
A pure value type derived from state. UI consumes it; tests parse it. |
| Adapter | Sources/DomainLogic/Adapters/Adapter.*.swift |
Every external dependency (disk, search service) behind a protocol — fully mockable. |
| Fluent tests | Tests/DomainLogicTests/Scaffolding/Scenario*.swift |
Tests read like user actions, drive the app via intents, observe via ViewRep. |
The rules behind the pattern live in ADR/ as numbered Architecture
Decision Records. Each ADR captures one rule (e.g. StateMachine as one-way
pipeline, Swift Concurrency only, Drive via Intent, observe via
ViewRep) with its reasoning and a ## Review Scope section that defines
exactly what counts as a violation — and what explicitly does not. The
ADRs are the spec; the table above is just a map.
| ADR | Topic |
|---|---|
| ADR-001 | StateMachine as one-way pipeline |
| ADR-002 | Fluent unit tests |
| ADR-003 | Drive via Intent, observe via ViewRep |
| ADR-004 | State-driven UI via ViewRep |
| ADR-005 | Adapter pattern for system I/O |
| ADR-006 | Swift Concurrency only (no GCD / Combine) |
| ADR-007 | SwiftUI for UI |
| ADR-008 | Predicate-driven ViewRep waits |
| ADR-009 | ViewRep consumers fail fast |
| ADR-010 | Swift Testing only (no XCTest) |
| ADR-011 | Wire shapes locked against literal fixtures |
| ADR-012 | Cancellation is terminal |
| ADR-013 | StateMachine internal state is not queryable |
The repo ships a Claude Code slash command at
.claude/commands/adr-check.md that audits
pending changes against every ADR. In a Claude Code session at the repo root,
run:
/adr-check # audit Sources/ and Tests/ against all ADRs
/adr-check ADR-006 # only check Swift-Concurrency-only
/adr-check Sources/**/*.swift # scope by path glob
The command reads each ADR's ## Review Scope section as the authoritative
checklist, honours its Drops clauses (sanctioned exceptions are not
violations), and prints a per-ADR Markdown report with path:line citations.
It detects only — it does not propose fixes.
swift run MoviesToWatchApp # launches the SwiftUI app
swift test # runs the fluent test suiteThe app uses The Movie Database (TMDB) as its
production search backend. Without a token the app still runs — it falls back
to the small in-process BundledMovieSearch catalogue, which is enough to
exercise the UI and the architecture.
To wire the real TMDB API:
-
Create a free TMDB account at https://www.themoviedb.org/signup.
-
Open https://www.themoviedb.org/settings/api and request an API key for personal / developer use. Approval is usually instant.
-
From the same settings page, copy the API Read Access Token (the v4 bearer token — a long JWT-looking string, not the v3
api_key). -
Save the token to a file at the repository root:
cp tmdb-token.txt.example tmdb-token.txt # then paste your token into tmdb-token.txt (one line, no quotes)tmdb-token.txtis listed in.gitignoreso the secret never lands in a commit. The committedtmdb-token.txt.exampleis just a placeholder. -
Alternatively, set the
TMDB_BEARER_TOKENenvironment variable before launch — the env var wins over the file:TMDB_BEARER_TOKEN='eyJh…' swift run MoviesToWatchApp
The token resolution logic lives in Sources/MoviesToWatchApp/TMDBToken.swift.
If neither source is set, AppState logs a notice on launch and uses
BundledMovieSearch.
Tests/DomainLogicTests/MoviesToWatchTests.swift is the best place to start.
Every test reads top-to-bottom like a user transcript:
@Test func addingMovieAppearsInToWatchList() async throws {
try await ScenarioForMoviesToWatch
.freshLaunch()
.addMovie(title: "The Matrix", year: 1999)
.expectMovieInList(title: "The Matrix", watched: false)
}No mocks are wired by the test author; the scenario factory wires the
simulated adapters once. The test never reaches into StateMachine internals
— it drives via intents and observes via ViewRep.values.