← Back to list

Swift Concurrency — AsyncSequence and AsyncStream (Part 7)

reference: WWDC21: Meet AsyncSequence | Apple

Hobin · 2026-05-23 10:27 · 0 claps · 4.3 min read
#swift #swift-concurrency #asyncsequence #async-streams #ios-development
Open on Medium ↗
Wiki topics: 📱 · Mobile Development

Swift Concurrency — AsyncSequence and AsyncStream (Part 7)

reference: WWDC21: Meet AsyncSequence | Apple

While studying Swift Concurrency, I found AsyncSequence especially interesting because it connects two familiar ideas: sequences and asynchronous work.

In this post, I’ll organize what AsyncSequence is, how to iterate over it, where Apple already provides async sequences, and how AsyncStream can convert callback or delegate-based APIs into Swift Concurrency style code.

Photo by Roshan lanova on Unsplash

Photo by Roshan lanova on Unsplash

What Is AsyncSequence?

AsyncSequence is essentially a sequence with async behavior. Like a normal Sequence, it produces values in order. But unlike a normal sequence, each element may become available later.

So iteration can suspend and resume.

An AsyncSequence has a few important characteristics:

  • values are produced in order
  • iteration can suspend while waiting for the next value
  • the sequence can finish normally
  • the sequence can throw an error if it is an AsyncThrowingSequence

Iterating with for-await

The standard way to consume an async sequence is for await.

For example:

for await quake in quakes {
    if quake.magnitude > 3 {
        displaySignificantEarthquake(quake)
    }
}

If the sequence can throw, use for try await:

do {
    for try await quake in quakes {
        if quake.magnitude > 3 {
            displaySignificantEarthquake(quake)
        }
    }
} catch {
    // handle error
}

Just like a normal loop, you can use break and continue.

Using an Iterator Directly

You can also create an async iterator manually:

var iterator = quakes.makeAsyncIterator()

while let quake = await iterator.next() {
    if quake.magnitude > 3 {
        displaySignificantEarthquake(quake)
    }
}

The iterator’s next() method returns the next value asynchronously.

When there are no more values, it returns nil.

Long-Running Async Sequences

Some async sequences may run for a very long time.

For example, a stream of earthquake updates or notifications may continue indefinitely.

In that case, it can be useful to wrap the iteration in a task and keep a reference to that task:

let quakeTask = Task {
    for await quake in quakes {
        if quake.magnitude > 3 {
            displaySignificantEarthquake(quake)
        }
    }
}

// Later...
quakeTask.cancel()

This makes cancellation explicit.

When the stream is no longer needed, the task can be canceled safely.

AsyncSequence in Apple APIs

Apple already provides async sequence APIs in several places.

FileHandle

FileHandle can expose bytes as an async sequence.

For example, standard input can be read line by line:

for try await line in FileHandle.standardInput.bytes.lines {
    // handle line
}

URL

URL can also provide line-based async reading:

let url = URL(fileURLWithPath: "/tmp/somefile.txt")

for try await line in url.lines {
    // handle line
}

URLSession

URLSession can return response bytes as an async sequence:

let (bytes, response) = try await URLSession.shared.bytes(from: url)

guard let httpResponse = response as? HTTPURLResponse,
      httpResponse.statusCode == 200 else {
    throw MyNetworkingError.invalidServerResponse
}

for try await byte in bytes {
    // handle byte
}

This is useful when data arrives progressively instead of as one complete value.

Notifications

NotificationCenter can expose notifications as an async sequence too:

let center = NotificationCenter.default

let notification = await center.notifications(
    named: .NSPersistentStoreRemoteChange
).first {
    $0.userInfo?[NSStoreUUIDKey] as? String == storeUUID
}

This makes notification handling feel much closer to normal Swift control flow.

Manipulating AsyncSequence Values

Many familiar sequence-style operations also exist for async sequences.

For example:

  • map
  • filter
  • compactMap
  • reduce
  • contains
  • first
  • prefix
  • dropFirst
  • zip

This is one of the nice parts of AsyncSequence.

Instead of manually wiring callbacks, you can describe the stream transformation directly.

For example:

let significantQuakes = quakes.filter { quake in
    quake.magnitude > 3
}

for await quake in significantQuakes {
    displaySignificantEarthquake(quake)
}

Introducing AsyncStream

So far, we looked at async sequences that already exist.

But what if we have an old callback-based or delegate-based API?

This is where AsyncStream becomes useful.

AsyncStream lets us create an AsyncSequence manually by yielding values through a continuation.

The initializer looks conceptually like this:

public struct AsyncStream<Element>: AsyncSequence {
    public init(
        _ elementType: Element.Type = Element.self,
        bufferingPolicy limit: Continuation.BufferingPolicy = .unbounded,
        _ build: (Continuation) -> Void
    )
}

The continuation is used to:

  • emit values with yield
  • finish the stream with finish
  • clean up resources with onTermination

Converting a Callback API to AsyncStream

Suppose we have an existing monitor like this:

final class QuakeMonitor {
    var quakeHandler: ((Quake) -> Void)?

    func startMonitoring() {
        // start
    }

    func stopMonitoring() {
        // stop
    }
}

Before AsyncStream, we might use it like this:

let monitor = QuakeMonitor()

monitor.quakeHandler = { quake in
    // handle quake
}

monitor.startMonitoring()

// Later...
monitor.stopMonitoring()

With AsyncStream, we can wrap it like this:

let quakes = AsyncStream(Quake.self) { continuation in
    let monitor = QuakeMonitor()

    monitor.quakeHandler = { quake in
        continuation.yield(quake)
    }

    continuation.onTermination = { _ in
        monitor.stopMonitoring()
    }

    monitor.startMonitoring()
}

Now the callback-based API becomes an async sequence:

let significantQuakes = quakes.filter { quake in
    quake.magnitude > 3
}

for await quake in significantQuakes {
    displaySignificantEarthquake(quake)
}

This is much easier to compose with the rest of Swift Concurrency.

Finishing and Cancellation

If the stream has a natural end, call continuation.finish().

If the stream can fail, use AsyncThrowingStream instead.

For example, the consumer side would use for try await:

do {
    for try await value in stream {
        // handle value
    }
} catch {
    // handle error
}

onTermination is also important.

When the consuming task is canceled, onTermination can run and clean up resources like timers, network requests, delegates, or monitors.

This is the part that makes AsyncStream practical in real code.

Without cleanup, the old callback source might keep running even after the async sequence is no longer being consumed.

Wrap-up

Today, we looked at AsyncSequence and AsyncStream.

The biggest takeaway for me is that AsyncSequence lets us model asynchronous values over time using familiar sequence-like syntax.

Here is the final summary:

  • AsyncSequence is like Sequence, but each value can arrive asynchronously.
  • for await and for try await are the standard ways to consume async sequences.
  • Long-running streams can be wrapped in a task and canceled later.
  • Apple provides async sequence APIs for files, URLs, URLSession bytes, and notifications.
  • Async sequences can be transformed with familiar operations like filter, map, and reduce.
  • AsyncStream is useful for converting callback or delegate-based APIs into async sequences.
  • onTermination should be used to clean up resources when a stream finishes or is canceled.
  • AsyncThrowingStream is used when the stream needs to emit errors.

If you found this useful, feel free to leave a comment or clap — it means a lot 😊

If you’d like to read the original post in Korean, check it out here: 👉 한국어 블로그 원문


메타데이터
post_id
682784dfcba0
slug
swift-concurrency-asyncsequence-and-asyncstream-part-7-682784dfcba0
url
https://medium.com/@hobin1019/swift-concurrency-asyncsequence-and-asyncstream-part-7-682784dfcba0
canonical_url
https://medium.com/@hobin1019/swift-concurrency-asyncsequence-and-asyncstream-part-7-682784dfcba0
author_url
https://medium.com/@hobin1019
status
ok
fetched_at
2026-06-09 15:37:30