Skip to Content
Course content

67: AsyncSequence and AsyncStream

Click on the "Edit" button in the top corner of the screen to edit your slide content.

I remember working with a developer a couple of years ago who was building a real-time stock ticker app. He had implemented a WebSocket listener using the traditional delegate pattern, but his view model had become a sprawling mess of state flags. He had booleans like isConnecting, isListening, and lastReceivedTimestamp, all just to keep track of a stream of data that was arriving at unpredictable intervals. He felt like he was fighting the language to do something that should have been simple: "I just want to loop over the updates as they come in."

That's exactly where AsyncSequence comes in. If a standard Sequence is a collection of items already sitting in memory, an AsyncSequence is a collection of items that arrive over time. Instead of a function returning a whole array at once, it returns a sequence that you can iterate over, but the loop pauses—suspends—every time it waits for the next element to be delivered.

Thinking of Sequences as Timelines

The most powerful part of this is the for await...in loop. You've already used await for single asynchronous returns, but when you combine it with a loop, you're essentially telling Swift: "Run this block of code whenever a new value arrives, and stay suspended in the meantime so you don't block the thread."

// Imagine a sequence that emits a number every second
for await number in tickingClockSequence {
    print("The time is now \(number)")
}
// This line isn't reached until the sequence finishes or is cancelled
print("Clock stopped.")

You'll encounter AsyncSequence in the wild quite often. For instance, URLSession.bytes(from:) returns an AsyncBytes sequence, allowing you to process a large file download line-by-line or byte-by-byte without loading the entire file into RAM. It transforms a "big bang" delivery of data into a steady stream.

Bridging Callbacks with AsyncStream

In the real world, you aren't always lucky enough to be using a library that already provides an AsyncSequence. Most of the time, you're dealing with legacy APIs, NotificationCenter, or delegate-based frameworks like CoreLocation. This is where AsyncStream becomes your best friend. It's a concrete implementation of AsyncSequence that lets you "yield" values into a stream from a non-async context.

Think of it as a pipe. You create the pipe, give the "input" end to your delegate or callback, and you hold onto the "output" end as an AsyncSequence. Here is how I usually handle those annoying delegate patterns:

class LocationProvider {
    // We wrap the delegate callback in an AsyncStream
    var locationUpdates: AsyncStream<CLLocation> {
        AsyncStream { continuation in
            let manager = CLLocationManager()
            let delegate = LocationDelegate { location in
                // This "yields" the value to the for-await loop
                continuation.yield(location)
            }
            manager.delegate = delegate
            manager.startUpdatingLocation()
            
            // Handle cleanup when the loop is cancelled
            continuation.onTermination = { @Sendable _ in
                manager.stopUpdatingLocation()
            }
        }
    }
}

I love this pattern because it moves the complex state management out of your business logic and into the stream itself. Your UI code no longer needs to care about CLLocationManagerDelegate; it just iterates over locationUpdates. It turns a "push" based API (the delegate pushing data at you) into a "pull" based API (you awaiting the next value), which is significantly easier to reason about and test.




📋 Practical Task

Build a Real-Time System Log Tailer

Your goal is to create a LogTailer class that simulates reading a log file where new lines are appended at random intervals. Instead of using a timer to poll for changes, you will use an AsyncStream to emit new log entries as they "arrive."

Requirements:

  • Create a class LogTailer with a property logs that is an AsyncStream<String>.
  • Inside the AsyncStream closure, use a Task and a while loop to simulate a log generator. Use try await Task.sleep(nanoseconds:) with a random duration (between 0.5 and 2 seconds) before yielding a string like "Log entry at [timestamp]".
  • Implement a stop() mechanism using the continuation.finish() method to gracefully end the stream.
  • In your main execution block, use a for await loop to print every log entry to the console as it arrives.
  • Ensure that you handle the termination of the stream so the program doesn't run infinitely if the task is cancelled.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.