A synchronous Sequence supplies elements during ordinary iteration. AsyncSequence allows the next element to require asynchronous work, so consumers can wait for values arriving over time. AsyncStream is a convenient bridge for producing asynchronous values through a continuation, often from callback-driven sources.
Review the concurrency lesson. This guide uses an explicit async entry point and the makeStream convenience introduced in Swift 5.9. It separates iteration, buffering and producer ownership so a stream does not become an unbounded queue or a callback subscription that nobody closes.
For await requests values over time
For await iterates an AsyncSequence whose next operation can suspend. A throwing sequence requires for try await and an error policy. Suspension lets the task wait without treating every value as already stored in an array. It does not automatically make producer work run on another thread or make shared mutation safe.
An async sequence may be finite, ongoing, throwing or cancellation-sensitive according to its concrete contract. Do not assume repeated iteration replays the same values, and do not collect an unbounded source into an ever-growing array merely because arrays are familiar.
The consumer's task lifetime matters. Leaving a screen or cancelling an operation should have a deliberate relationship to the sequence and any underlying subscription. Async syntax describes waiting; it does not by itself release a delegate, observer, timer or hardware resource owned elsewhere.
A continuation is a producer interface
Yield offers a value to the stream, and finish signals completion. Yield returns information about acceptance, dropping or termination according to the API, which a serious bridge should consider. Calling yield after termination does not revive the stream. Treat the continuation as a controlled capability owned by the producer.
AsyncThrowingStream adds failure completion when the bridge must communicate an error. Keep normal completion, cancellation and failure distinguishable where callers need them. A callback reporting an error should not silently finish a success-only stream if that hides why expected data stopped.
The continuation APIs support their documented concurrent use, but the objects your callbacks access need their own Sendable or isolation design. A stream does not grant thread safety to arbitrary captured state. Review how the callback source invokes handlers and ensure producer cleanup can be performed from the appropriate context.
Choose buffering according to value meaning
The default stream buffering can retain all queued values. That is convenient for a small finite demonstration but risky for an ongoing fast source with a slow consumer. BufferingNewest keeps newer values within a capacity, while bufferingOldest retains earlier queued values and can discard newer arrivals.
A progress display may care mainly about the latest state, while a payment event stream cannot casually discard transactions. Select capacity and drop behavior from the domain. Buffering is not automatically backpressure: yield is not a suspending operation that waits for the consumer to catch up.
Inspect the yield result when drops matter, and consider a different channel or protocol if the producer must slow down without losing values. A bounded queue prevents unbounded retention but does not guarantee complete delivery. Document whether values are state snapshots, replaceable observations or durable events requiring stronger persistence guarantees.
Termination must release the underlying source
OnTermination can connect stream termination to cleanup of the callback subscription. The handler's execution context and sendability requirements matter, so do not mutate UI state or nonisolated shared fields arbitrarily inside it. Arrange cleanup through the resource's supported owner when necessary.
Avoid strong-reference cycles among a producer, continuation and subscription handler. Keep a clear cancellation token or owner, and ensure the producer does not retain the consumer forever. A callback bridge should document who starts the source, who stops it, and whether there can be several independent consumers.
Do not assume that breaking one loop is a complete universal resource-management policy for every asynchronous source. Follow the concrete stream and producer contract and explicitly cancel owned tasks or subscriptions as needed. Tests should verify cleanup with a fake callback source as well as verify emitted values.
Official references
The Swift concurrency guide covers asynchronous sequences. The AsyncStream factory proposal documents makeStream, and the AsyncStream API describes continuation and buffering behavior. Review finite completion, failure, cancellation, drops and source cleanup separately before connecting a real callback system.
Swift AsyncSequence and AsyncStream Examples
Each example shows the code, the output it produces and what each part does.
Finish a finite stream
The producer supplies two values and signals normal completion. The consumer drains them and exits its loop. Compile a standalone async entry point with swiftc -parse-as-library Demo.swift. This finite example does not need a background task, so its output is not a timing-sensitive claim about competing producers.
@main struct Demo {
static func main() async {
let pair = AsyncStream<Int>.makeStream()
pair.continuation.yield(2)
pair.continuation.yield(4)
pair.continuation.finish()
for await value in pair.stream {
print(value)
}
print("finished")
}
}2
4
finishedSwift AsyncSequence and AsyncStream in Real Projects
Worked exercise: Keep only the latest queued counts
Produce three counts before consumption starts, with capacity two and a newest-value policy. This controlled ordering demonstrates dropping without relying on scheduler speed. Finish the stream and sum the values actually retained.
@main struct Demo {
static func main() async {
let pair = AsyncStream<Int>.makeStream(bufferingPolicy: .bufferingNewest(2))
pair.continuation.yield(1)
pair.continuation.yield(2)
pair.continuation.yield(3)
pair.continuation.finish()
var total = 0
for await value in pair.stream {
print(value)
total += value
}
print("total:\(total)")
}
}2
3
total:5One is discarded because the queue is already bounded when three arrives. In an ongoing concurrent source, the precise observed subset can depend on timing, so do not promise this exact sequence without the exercise's controlled production-before-consumption setup. Test both policies and record dropped values when they matter to diagnosis.
Swift AsyncSequence and AsyncStream FAQ
Does yield wait until a consumer processes the value?
No. AsyncStream continuation yield is a producer offer governed by the buffering and termination contract, not a suspending acknowledgment that the consumer finished its work. A slow consumer can leave values queued or cause drops under a bounded policy. If the domain needs producer backpressure or processing acknowledgments, design an explicit protocol or choose a suitable abstraction. Do not infer durable delivery from the fact that yield was called successfully.
Which buffering policy is best for progress updates?
A newest-value policy can fit replaceable progress state because an old percentage is often less useful than the current one. The correct choice still depends on the product's meaning and capacity requirements. For a durable event history, dropping either old or new values may be unacceptable. Separate latest-state presentation from complete event recording when both are needed, and inspect yield results if losing observations needs to be reported or measured.
How does a stream stop a callback subscription?
The bridge must connect termination or an explicit cancellation owner to the subscription's cleanup mechanism. OnTermination can help, but it does not know how to unregister a delegate or stop a device by itself. Follow the source's context and sendability requirements, avoid ownership cycles, and retain a cancellation token where appropriate. A good test uses a fake source to verify that cleanup actually happens when the consumer's lifetime ends.
Can several tasks consume one stream as a broadcast?
Do not assume an arbitrary AsyncSequence provides broadcasting or identical replay to multiple consumers. Concrete sources have their own iterator and sharing contracts. If every subscriber must receive every event, design a supported broadcast abstraction with explicit buffering and subscriber lifetime rules. Reusing one stream object in several tasks is not enough to establish those guarantees. Decide whether values are divided, replayed or copied, and test that contract directly.
Swift AsyncSequence and AsyncStream Exercises
Answer each one and check it instantly. If you get stuck, open the hint before the answer.
A source emits ongoing replaceable state faster than consumption. What needs an explicit choice?
Swift AsyncSequence and AsyncStream Quiz
2 questions to check what you learned.
What does for await add to sequence iteration?
- Automatic deep copying of every shared object.
- A promise that every value is already stored in an array.
- The next element can require asynchronous waiting.
What does finish do for the finite example?
- Signals normal completion so iteration can end after retained values are consumed.
- Permanently stores all values in a database.
- Starts a second consumer thread.