Chapter 8 — AsyncSequence and AsyncStream
AsyncSequence: a sequence whose elements arrive over time
// Each line arrives as the file downloads; the loop suspends between lines.
for try await line in url.lines {
print(line)
}
// Async transforms compose just like the synchronous ones.
for await bigNumber in numberStream.filter({ $0 > 100 }).prefix(10) {
print(bigNumber)
}
AsyncStream: producing a sequence from callbacks
// LiveTicker/PriceFeed.swift — bridge a repeating callback into an AsyncStream.
func priceUpdates(for symbol: String) -> AsyncStream<Price> {
AsyncStream { continuation in
let subscription = PriceService.shared.subscribe(symbol) { price in
continuation.yield(price) // emit each tick as it arrives
}
continuation.onTermination = { _ in
subscription.cancel() // clean up when the stream ends/cancels
}
}
}
for await price in priceFeed.priceUpdates(for: "AAPL") {
updateChart(with: price) // runs every time a new price is yielded
}
continuation.yield(value)— push one element into the stream. Call it as many times as you like.continuation.finish()— end the stream (thefor awaitloop exits). Usefinish(throwing:)withcontinuation.onTermination— a closure that runs when the stream terminates, whether it
flowchart LR
SRC["callback source
(timer / delegate / service)"] -->|"each event"| Y["continuation.yield(value)"] Y --> LOOP["for await value in stream"] STOP["stream ends / consumer cancels"] --> OT["continuation.onTermination
(tear down source)"]
(timer / delegate / service)"] -->|"each event"| Y["continuation.yield(value)"] Y --> LOOP["for await value in stream"] STOP["stream ends / consumer cancels"] --> OT["continuation.onTermination
(tear down source)"]
The makeStream factory
// Get the stream and continuation as a pair — yield from anywhere.
let (stream, continuation) = AsyncStream.makeStream(of: Price.self)
// Somewhere else entirely — a delegate, a Combine sink, a notification handler:
func didReceive(_ price: Price) {
continuation.yield(price)
}
// The consumer:
for await price in stream { updateChart(with: price) }
Buffering: what happens to values with no consumer yet
AsyncStream(bufferingPolicy: .bufferingNewest(1)) { continuation in … }
Making your own AsyncSequence from scratch
// A custom AsyncSequence that pages through an API until there are no more pages.
struct PagedResults<Item: Decodable & Sendable>: AsyncSequence {
typealias Element = [Item]
let firstPageURL: URL
let client: APIClient
struct Iterator: AsyncIteratorProtocol {
var nextURL: URL?
let client: APIClient
mutating func next() async throws -> [Item]? {
guard let url = nextURL else { return nil } // no more pages → end sequence
let page: Page<Item> = try await client.fetch(Page<Item>.self, from: url)
nextURL = page.nextURL
return page.items
}
}
func makeAsyncIterator() -> Iterator { Iterator(nextURL: firstPageURL, client: client) }
}
// Consume it like any sequence — each iteration fetches the next page.
for try await pageOfItems in PagedResults(firstPageURL: url, client: client) {
append(pageOfItems)
}
Wiring LiveTicker together
// LiveTicker/TickerViewModel.swift
@MainActor @Observable
final class TickerViewModel {
private(set) var latestPrice: Price?
private var streamTask: Task<Void, Never>?
func start(symbol: String) {
streamTask = Task {
// Inherits @MainActor, so assigning to latestPrice is safe.
for await price in PriceService.shared.priceUpdates(for: symbol) {
latestPrice = price // UI updates on each tick
}
// Loop ends when the stream finishes; task ends naturally.
}
}
func stop() { streamTask?.cancel() } // cancels the loop → onTermination tears down the feed
}
What we built in this chapter
- LiveTicker, which turns a repeating price callback into an
AsyncStreamconsumed by a plain AsyncSequenceas "a sequence whose elements arrive over time," consumed withfor await/AsyncStreamproduction viayield,finish, and the crucialonTermination- Buffering policies (
.unbounded/.bufferingNewest/.bufferingOldest) as the knob for - Building a custom
AsyncSequence(paginated results) viaAsyncIteratorProtocol.next(), and the
Mental model to take away
- An
AsyncSequenceis an array you iterate over time —for awaitsuspends for each element and AsyncStreambridges repeating callbacks (the many-times counterpart to Chapter 7's one-shot- Choose a buffering policy to handle backpressure — a fast, infinite source with the default
- Cancelling the consuming task ends the stream and fires
onTermination, giving you clean,