A small Swift 6 command-line lab for a 30-minute discussion about long-lived
for await loops. It turns the claims in Michael Long's
Using Async For/Await? You're Probably Doing It Wrong
into observable code and tests.
The point is not that for await is unsafe. The point is that convenient
syntax does not define ownership, termination, producer cleanup, buffering, or
actor isolation for you.
- macOS 14 or newer
- A Swift 6 toolchain
- Xcode if you want to present with editor tabs and Command-R
There are no third-party dependencies.
From Terminal:
swift run AsyncSequenceLabDemo long-lived-task
swift run AsyncSequenceLabDemo weak-self-trap
swift run AsyncSequenceLabDemo owned-task
swift run AsyncSequenceLabDemo producer-termination
swift run AsyncSequenceLabDemo backpressure
swift run AsyncSequenceLabDemo latest-value-winsRun every chapter as a quick rehearsal:
swift run AsyncSequenceLabDemo allValidate the observations:
swift testFor Xcode, open Package.swift, select the AsyncSequenceLabDemo scheme and
the local Mac destination, then change selectedDemo in
Sources/AsyncSequenceLabDemo/AsyncSequenceLabDemo.swift before pressing
Command-R.
| Chapter | What the running code proves |
|---|---|
long-lived-task |
A live task can retain its captured owner until the sequence finishes. |
weak-self-trap |
An outer guard let self recreates a strong reference across every suspension; a weak per-event access releases the owner but does not terminate the task. |
owned-task |
Storing and cancelling the task handle works when the task closure does not strongly retain its owner. |
producer-termination |
Consumer cancellation must reach callback-side resources through onTermination. |
backpressure |
Buffering policy decides which values wait, disappear, or accumulate. |
latest-value-wins |
Cancelling the previous operation ensures only the newest value completes when handlers cooperate with cancellation. |
The examples use controlled streams and weak lifetime probes so the results are
deterministic rather than timing anecdotes. The same observations are encoded
as Swift Testing assertions in Tests/AsyncSequenceLabTests.
The opening example definitely risks keeping an object alive indefinitely, but
"reference cycle" is not always the most precise diagnosis. A live unstructured
task is enough to retain its closure, which can retain self; an object-to-object
cycle is not required. A real cycle does exist when self stores the task handle
and the task closure strongly captures self:
self -> task handle -> task closure -> self
That distinction matters because [weak self] can fix the ownership edge while
still leaving an orphaned task suspended forever. The review question is not
only "is the capture weak?" but also "who owns this task, and what ends it?"
DEMO_SCRIPT.md is the presenter guide. It contains:
- a rehearsed 30-minute timeline;
- prediction prompts before each run;
- expected console output;
- review rules and likely senior-level questions;
- optional SwiftUI, actor-isolation, detached-task, and latest-value-wins appendices;
- a recovery path if Xcode misbehaves during the session.
The main talk deliberately uses five small files instead of one large sample so each lifecycle decision remains visible in code review.
For a browser-rendered version with a table of contents, cue styling, dark mode,
and print/PDF support, open DEMO_SCRIPT.html. Regenerate it
after editing the Markdown source with:
node Scripts/render-demo-script.cjsFor the live session, DEMO_NOTES.html is a compact
landscape one-pager containing only the timing, prompts, expected observations,
takeaways, and recovery commands.