Streaming is available in most browsers,
and in the Developer app.
-
Meet AsyncSequence
Iterating over a sequence of values over time is now as easy as writing a “for” loop. Find out how the new AsyncSequence protocol enables a natural, simple syntax for iterating over anything from notifications to bytes being streamed from a server. We'll also show you how to adapt existing code to provide asynchronous sequences of your own. To get the most out of this session, we recommend first watching “Meet async/await in Swift.”
Resources
- SE-0298: Async/Await: Sequences
- SE-0314: AsyncStream and AsyncThrowingStream
- The Swift Programming Language: Concurrency
Related Videos
WWDC22
- Compose advanced models with Create ML Components
- Create a more responsive media app
- Meet Swift Async Algorithms
WWDC21
-
Download
♪ Bass music playing ♪ ♪ Philippe Hausler: Hi, my name is Philippe, and I am really excited to introduce you to a cool new feature in Swift, AsyncSequence.
Today we'll go over what async sequences are and the fundamentals behind them.
Then we'll talk about how you can use them in your code and go over a few of the new AsyncSequence APIs.
And finally, we'll explore how you can build your own async sequences.
So let's dive right in.
This is a really simple tool I wrote to illustrate some of the cool new stuff you can do with AsyncSequence.
In this tool, we start off with a URL to an endpoint.
It lists recent earthquakes.
Now, normally downloading stuff is really an asynchronous task that can take some time.
But in this case, we don't want to wait for all the things to download; instead, we want to show things as they are received.
So I decided to shake things up a bit and use the new async/await features to get the lines responded from this endpoint.
The data that we're fetching is formatted as comma-separated text, so each line is a complete row of data.
Since the async sequence of lines is emitting each line as it's received, that means we can potentially have a really large download ahead of us.
But by emitting them as we get them, the snippet feels really responsive, and the most awesome part about it is that you can use the same things that you're familiar with using from regular sequences in this new async context.
That means that you can use the new for-await-in syntax to iterate, and functions like map, filter, and reduce; or -- like in this sample -- the dropFirst function to manipulate those values.
So how does this work? Well, a lot of things that I'm going to talk about today have their groundwork based in the async/await talk.
But let's recap a few key points.
Async functions let you write concurrent code without the need for callbacks, by using the await keyword.
Calling an async function will suspend and then be resumed whenever a value or error is produced.
AsyncSequence, on the other hand, will suspend on each element and resume when the underlying iterator produces a value or throws.
Basically, as the name implies, they're just like regular sequences but with a few key differences.
Namely, each element is delivered asynchronously.
But because they are an asynchronous delivery, that means that failure is definitely a possibility.
Some async sequences throw, but if failure is not an option, others do not.
Just like functions that throw, the compiler will help make sure that errors are handled when iterating or composing.
Generally speaking, asynchronous sequences are a description of how to produce values over time.
So an async sequence may be zero or more values and then signify completion with returning a nil from its iterator, just like sequences.
When an error occurs, that's also a point at which the async sequence is at a terminal state, and after an error happens, they'll return nil for any subsequent calls to next on their iterator.
So let's take a dive into how that definition works by first starting off with regular iteration.
Here we have a pretty familiar pattern.
This is a for-in loop.
In this case, it's iterating quakes from a sequence and then calling a function when the magnitude is above a certain value.
The compiler has knowledge of how this iteration should work, but what it does isn't magic.
The compilation step really just does some straightforward transformations.
Let's examine those transformations so you can understand what the asynchronous form of this is.
This is roughly what the compiler does when building the previous code.
It first starts off by creating an iterator variable and then uses a while loop to get every quake produced by the iterator when next is invoked.
To use the new async/await functionality, there is one slight alteration that can be made.
It's as simple as changing that next function to be an asynchronous one.
We can now have the iteration participate in Swift concurrency by awaiting the next quake.
Let's rewind back to uncover what this would have been like if the loop was on an async sequence.
As previously mentioned, we need to await each item out of the async sequence.
This is reflected in the new for-await-in syntax.
This all means that if you know how to use Sequence, you already have a good idea on how to use AsyncSequence.
There are a few ways you can utilize async sequences.
As we just covered, you can use the new for-await-in syntax; or if the async sequence throws, you can use the new for-try-await-in syntax.
This lets you easily iterate the values produced asynchronously -- without having to muck about with closures -- and use the syntax you are already familiar with to iterate through them.
Even things like break and continue just work.
Now you have now had a good introduction to the theory of async sequences.
Let's explore that iteration just a bit further.
Given a source that is an async sequence, you can await each value by using the for-await-in syntax.
This means that it will await each item produced by the iterator, and when it would hit a terminal, it completes the loop.
When iterating async sequences, breaking is obviously a good way to terminate iteration early from inside the loop.
This works just like regular sequences.
Here we're breaking when the location data is not present for the quake.
Or if we have some of value we want to skip, we can use continue.
In this case, if the depth is greater than some value, we're skipping those and continuing on to await the next quake.
This next iteration from the download works just like before, but in this case, the source that we have can throw an error.
Just like throwing functions, try is required to process each element when the AsyncSequence being iterated can throw.
And also just like throwing functions, the compiler will detect when you're missing a try and offer you a Fix-it to correct the mistake.
This means that when an async sequence can produce errors, you're always safe, because the language forces you to either throw that error or catch it.
This second iteration runs sequentially after the iteration of the first loop.
Running code sequentially isn't always what's desired.
If it's useful to run the iteration concurrent to other things going on, you can create a new async task that encapsulates the iteration.
This can be useful when you know the async sequences you're using might run indefinitely.
Now, even though that sequence could potentially be indefinite, it's considerably less common to occur.
But in the world of asynchronous behavior, it is something much more common and something that you need to consider when using them.
Thankfully, the facilities for concurrency in Swift make this really easy and safe to do.
This can be also really helpful when you want to potentially cancel the iteration externally.
Here we can run the two iterations concurrently and terminate the iteration later on.
It's pretty easy with tasks to scope the work of an iteration that might be indefinite to the lifetime of some container.
Let's next take a tour of some of the AsyncSequence APIs that are available as of macOS Monterey, iOS 15, tvOS 15, and watchOS 8.
There are a lot of them, but I'll show you some of the highlights.
Reading from files is often a prime use case for asynchronous behavior.
FileHandle now has a new bytes property that gives access to an asynchronous sequence of bytes from that FileHandle.
This can be used in conjunction with the new extension on AsyncSequence that converts asynchronous sequences of bytes into lines.
But dealing with files is so common that we decided that URL should have accessors for both bytes and lines.
This is that same API that I used in the initial example.
It's a convenience property on URL to return an AsyncSequence of lines from the contents, either from a file or from the network.
I'm certain this will make a number of previously really complicated tasks easy and safe.
Sometimes getting things from the network requires a bit more control over the responses and authentication.
So URLSession now has a bytes function to fetch an async sequence of bytes given a URL or URLRequest.
If you want to know more, you should definitely check out the session "Use async/ await with URLSession" for some more details on this and a whole lot more new asynchronous capabilities with URLSession.
But files and networks are not the only thing that makes sense for AsyncSequence.
Notifications can be now awaited with the new notifications API.
And iteration is not the only way to interact with an AsyncSequence.
In this example, we're awaiting the first notification for remote changes that has a store UUID that matches.
Using methods like firstWhere, along with the notifications async sequence, allows for some really neat new design patterns that can make code that was previously expressing complicated logic now compact and easier to read.
And if all of those weren't cool enough, there are a whole lot of new APIs for asynchronously manipulating values from async sequences.
These should be pretty familiar, because they're some of the same functions that are available on Sequence.
We've already covered a few so far, like dropFirst and firstWhere, but there are a whole lot more than just those.
Pretty much anything you can think of for using on Sequence now has an asynchronous counterpart for working with AsyncSequence.
Now that was a lot to take in, and you might be thinking, "Hey, those new APIs are really cool, and that syntax is super neat, but how can I make my own async sequences?" Well, let's do exactly that! There are a few ways of implementing an async sequence, but I'm going to focus in on how to adapt your existing code.
Particularly, there are a few design patterns that work really well with AsyncSequence, and we have some fantastic facilities for making what you already have interact with this new concept.
Some of those design patterns are like closures that are called multiple times, but also some delegates can work nicely too.
Pretty much anything that does not need a response back and is just informing of a new value that occurs can be a prime candidate for making an async sequence.
These design patterns are really common and you likely already have a few in your apps today.
This is an example of a common handler pattern.
It's a class that has a handler property and a start and stop method.
It seems like a perfect candidate for AsyncSequence.
Existing usage might be something like this where a monitor is created, and a handler to get values is assigned, and then the monitor is started so that quakes can be sent to the handler.
Later on, the monitor might be stopped to cancel the events being produced.
We can use the same interface to adapt the usage to the new AsyncStream type.
It takes only a small bit of code to use it and allows you to build an async sequence.
When constructing an async stream, an element type and construction closure is specified.
The closure has a continuation that can yield values more than once, finish, or handle termination.
So this means, in this case, the monitor can be created inside the construction closure.
And then the handler can be assigned to yield quakes to the continuation.
And then the onTermination can handle the cancellation and cleanup.
And then we can start monitoring.
The same monitor code that we had before can easily be encapsulated within the async stream's construction.
This reduces the need to replicate the same logic in every use site.
And this is how the usage of this async stream would work.
You can use the powerful transformation functions -- like filter -- and the new for-await-in syntax.
This lets you focus on the intent of your code instead of needing to worry about replicating the bookkeeping, since everything is wrapped up into one place.
There's a lot of flexibility with AsyncStream to create your own async sequences.
This is really just one example and there are likely numerous others that you can adapt in your own code.
AsyncStream is a great way to adapt your existing code to become an async sequence.
It handles all of the things you would expect from an async sequence, like safety, iteration, and cancellation; but they also handle buffering.
AsyncStream is a solid way of building your own async sequences and a suitable return type from your own APIs, since the only source of elements being produced is from the construction.
And if you need to represent errors being thrown? Well, we have a type for that! AsyncThrowingStream that is just like AsyncStream but can handle errors.
It offers the same flexibility and safety as AsyncStream but can handle failures by throwing from its iteration.
AsyncSequence is a really powerful tool that is both safe and familiar for dealing with more than one asynchronous value.
If you know how to use Sequence, you already know how to use AsyncSequence.
We've gone over what async sequences are, and how they're used, and introduced you to AsyncStream.
We've dived into the theory and what defines them, and a few of the newly introduced async sequences, and finally, how to build your own.
I eagerly await what you do with them next.
♪
-
-
0:37 - QuakesTool
@main struct QuakesTool { static func main() async throws { let endpointURL = URL(string: "https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/all_month.csv")! // skip the header line and iterate each one // to extract the magnitude, time, latitude and longitude for try await event in endpointURL.lines.dropFirst() { let values = event.split(separator: ",") let time = values[0] let latitude = values[1] let longitude = values[2] let magnitude = values[4] print("Magnitude \(magnitude) on \(time) at \(latitude) \(longitude)") } } }
-
3:24 - Iterating a Sequence
for quake in quakes { if quake.magnitude > 3 { displaySignificantEarthquake(quake) } }
-
3:52 - How the compiler handles iteration
var iterator = quakes.makeIterator() while let quake = iterator.next() { if quake.magnitude > 3 { displaySignificantEarthquake(quake) } }
-
4:11 - How the compiler handles asynchronous iteration
var iterator = quakes.makeAsyncIterator() while let quake = await iterator.next() { if quake.magnitude > 3 { displaySignificantEarthquake(quake) } }
-
4:28 - Iterating an AsyncSequence
for await quake in quakes { if quake.magnitude > 3 { displaySignificantEarthquake(quake) } }
-
5:36 - Terminating iteration early by breaking
for await quake in quakes { if quake.location == nil { break } if quake.magnitude > 3 { displaySignificantEarthquake(quake) } }
-
5:51 - Skipping values by continuing
for await quake in quakes { if quake.depth > 5 { continue } if quake.magnitude > 3 { displaySignificantEarthquake(quake) } }
-
6:05 - AsyncSequence might throw
do { for try await quake in quakeDownload { ... } } catch { ... }
-
7:15 - Concurrently iterating inside an async task
let iteration1 = Task { for await quake in quakes { ... } } let iteration2 = Task { do { for try await quake in quakeDownload { ... } } catch { ... } } //... later on iteration1.cancel() iteration2.cancel()
-
7:56 - Reading bytes from a FileHandle
for try await line in FileHandle.standardInput.bytes.lines { ... }
-
8:16 - Reading lines from a URL
let url = URL(fileURLWithPath: "/tmp/somefile.txt") for try await line in url.lines { ... }
-
8:49 - Reading bytes from a URLSession
let (bytes, response) = try await URLSession.shared.bytes(from: url) guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 /* OK */ else { throw MyNetworkingError.invalidServerResponse } for try await byte in bytes { ... }
-
9:12 - Notifications
let center = NotificationCenter.default let notification = await center.notifications(named: .NSPersistentStoreRemoteChange).first { $0.userInfo[NSStoreUUIDKey] == storeUUID }
-
11:10 - Using an AsyncStream
class QuakeMonitor { var quakeHandler: (Quake) -> Void func startMonitoring() func stopMonitoring() } let quakes = AsyncStream(Quake.self) { continuation in let monitor = QuakeMonitor() monitor.quakeHandler = { quake in continuation.yield(quake) } continuation.onTermination = { @Sendable _ in monitor.stopMonitoring() } monitor.startMonitoring() } let significantQuakes = quakes.filter { quake in quake.magnitude > 3 } for await quake in significantQuakes { ... }
-
-
Looking for something specific? Enter a topic above and jump straight to the good stuff.
An error occurred when submitting your query. Please check your Internet connection and try again.