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 (the for await loop exits). Use finish(throwing:) with
  • continuation.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)"]

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 AsyncStream consumed by a plain
  • AsyncSequence as "a sequence whose elements arrive over time," consumed with for await /
  • AsyncStream production via yield, finish, and the crucial onTermination
  • Buffering policies (.unbounded / .bufferingNewest / .bufferingOldest) as the knob for
  • Building a custom AsyncSequence (paginated results) via AsyncIteratorProtocol.next(), and the

Mental model to take away

  • An AsyncSequence is an array you iterate over timefor await suspends for each element and
  • AsyncStream bridges 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,