Swift Concurrency — AsyncSequence and AsyncStream (Part 7)
reference: WWDC21: Meet AsyncSequence | Apple
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
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:
mapfiltercompactMapreducecontainsfirstprefixdropFirstzip
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:
AsyncSequenceis likeSequence, but each value can arrive asynchronously.for awaitandfor try awaitare 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, andreduce. AsyncStreamis useful for converting callback or delegate-based APIs into async sequences.onTerminationshould be used to clean up resources when a stream finishes or is canceled.AsyncThrowingStreamis 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