Working with Asynchronous Iterations Using AsyncSequence

AsyncLearn
3 min readDec 4, 2023

--

In some cases, it’s necessary to iterate over a sequence of values that are asynchronously emitted. Swift provides an API that allows us to do this easily using a simple for loop, thanks to the AsyncSequence protocol.

Features of AsyncSequence

  • It will pause at each iteration and resume when its execution is complete.
  • We can use continue and break, just like in any other loop.
  • You can use the same functions you’re accustomed to using with regular sequences, such as map, reduce, dropFirst...
  • It’s similar to a sequence, but asynchronous. Each element is delivered asynchronously.
  • It can throw an exception.
  • It terminates when it reaches the end or when an error occurs. If an error occurs, it will return nil for any subsequent next calls on its iterator.
  • If the asynchronous sequence can throw an exception, we use for try await in.
  • If we need to execute the iteration concurrently with other ongoing tasks, we can create a new asynchronous task that encapsulates the iteration. For example:
Task {
for await element in list {
...
}
}

Task {
for await element in anotherList {
...
}
}

In this case, we have two AsyncSequences, but by encapsulating them within a Task, each one can run on a different thread and not block the rest of the code in the function.

How to Cancel an Iteration

To do this, we need to have a reference to the Task and execute the cancel() method. Let's see an example:

let iterator = Task {
for await event in asyncEvents {
...
}
}

iterator.cancel()

Creating an AsyncSequence Using AsyncStream

We can create asynchronous sequences using AsyncStream. Let's see an example, starting with the ParkingMonitor class:

class ParkingMonitor {
var handler: ((Vehicle) -> Void)?
func start() {}
func stop() {}
}

The start() and stop() methods start and stop the tracking process for vehicles entering a parking lot. The handler allows us to assign a closure that receives a Vehicle.

To convert this class into an AsyncSequence, we use the following code:

let vehicles = AsyncStream(Vehicle.self) { continuation in
let monitor = ParkingMonitor()
monitor.handler = { vehicle in
continuation.yield(vehicle)
}
continuation.onTermination = { @Sendable _ in
monitor.stop()
}
monitor.start()
}

With this code, we create an instance of AsyncStream that accepts objects of type Vehicle, and we pass a closure in which we configure our ParkingMonitor. Thanks to continuation, we can use the yield(_ value:) method to emit values. Also, thanks to onTermination, we indicate that we want to call stop() on the monitor when we stop using this AsyncStream.

Optionally, we can pass the bufferingPolicy parameter. By default, it uses the .unbounded policy, which stores an unlimited number of elements in the buffer. You can also choose to store only the oldest (.bufferingOldest(Int)) or the newest (.bufferingNewest(Int)) elements, depending on your needs.

Thanks to having an AsyncStream, we can use for await to print the vehicle models that enter the parking lot:

for await vehicle in vehicles {
print(vehicle.model)
}

Swift APIs Using AsyncSequence

Some Swift APIs make use of AsyncSequence to make it easier to use them and leverage the new concurrency model. Here are some available APIs:

  • You can read bytes asynchronously from a FileHandle.
for try await line in FileHandle.standardInput.bytes.lines {
...
}
  • Read bytes asynchronously or read lines asynchronously from a URL:
let url = URL(fileURLWithPath: "/tmp/temp.txt")
for try await line in url.lines {
...
}
  • Read bytes asynchronously from a URLSession:
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 {
...
}
  • You can use async with the notification API:
let center = NotificationCenter.default
let notification = await
center.notifications(named: .NSPersistentStoreRemoteChange).first {
$0.userInfo[NSStoreUUIDKey] == storeUUID
}

If you want to read the Spanish version of this article, you can find it here: https://asynclearn.com/blog/trabajando-con-iteraciones-asincronas-usando-async-sequence/

--

--

AsyncLearn

Stay up-to-date in the world of mobile applications with our specialised blog.