AsyncSequence & AsyncStream Tutorial for iOS
Learn how to use Swift concurrency’s AsyncSequence and AsyncStream protocols to process asynchronous sequences. By Audrey Tam.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
AsyncSequence & AsyncStream Tutorial for iOS
20 mins
You’ve embraced async/await as the newest and safest way to code for concurrency in Swift. You’re loving how eliminating a lot of the nested completion handlers reduces the amount of code you write and simplifies that code’s logic so it’s easier to get it right.
And what’s the next step in your Swift concurrency journey? Asynchronous loops. Using Swift concurrency’s AsyncSequence and AsyncStream protocols, this is as easy as looping over an ordinary sequence.
In this tutorial, you’ll:
- Compare the speed and memory use when synchronously and asynchronously reading a very large file.
- Create and use a custom
AsyncSequence. - Create and use pull-based and push-based
AsyncStreams.
URLSession — and with basic Swift concurrency features like those presented in async/await in SwiftUI and SwiftUI and Structured Concurrency.
Getting Started
Use the Download Materials button at the top or bottom of this tutorial to download the starter project. Open it in Xcode to see what you have to work with.
Data Files
The purpose of ActorSearch is to help you solve puzzles that ask for actor names by searching the name.basics.tsv.gz dataset from IMDb Datasets. This file contains a header line to describe the information for each name:
-
nconst(string) – alphanumeric unique identifier of the name/person -
primaryName(string)– name by which the person is most often credited -
birthYear– in YYYY format -
deathYear– in YYYY format if applicable, else ‘\N’ -
primaryProfession(array of strings) – the top three professions of the person -
knownForTitles(array of tconsts) – titles the person is known for
To reduce the demand on your network and make it straightforward to read in line by line, the starter project already contains data.tsv: This is the unzipped name.basics.tsv.gz, with the header line removed. It’s a tab-separated-values (TSV) file, formatted in the UTF-8 character set.
In this tutorial, you’ll explore different ways to read the file contents into an array of Actor values. data.tsv contains 11,445,101 lines and takes a very long time to read in, so you’ll use it only to compare memory use. You’ll try out most of your code on the smaller files data-100.tsv and data-1000.tsv, which contain the first 100 and 1000 lines, respectively.
Models
Open ActorAPI.swift. Actor is a super-simple structure with only two properties: id and name.
In this file, you’ll implement different methods to read a data file. The ActorAPI initializer takes a filename argument and creates the url. It’s an ObservableObject that publishes an Actor array.
The starter contains a basic synchronous method:
func readSync() throws {
let start = Date.now
let contents = try String(contentsOf: url)
var counter = 0
contents.enumerateLines { _, _ in
counter += 1
}
print("\(counter) lines")
print("Duration: \(Date.now.timeIntervalSince(start))")
}
This just creates a String from the contentsOf the file’s url, then counts the lines and prints this number and how long it took.
enumerateLines(invoking:) is a StringProtocol method bridged from the NSString method enumerateLines(_:).
View
Open ContentView.swift. ContentView creates an ActorAPI object with a specific filename and displays the Actor array, with a search field.
First, add this view modifier below the searchable(text:) closure:
.onAppear {
do {
try model.readSync()
} catch let error {
print(error.localizedDescription)
}
}
You call readSync() when the view appears, catching and printing any errors readSync() throws.
Now, look at the memory use when you run this app. Open the Debug navigator, then build and run. When the gauges appear, select Memory and watch:

On my Mac, reading in this 685MB file took 8.9 seconds and produced a 1.9GB spike in memory use.
Next, you’ll try out a Swift concurrency way to read the file. You’ll iterate over an asynchronous sequence.
AsyncSequence
You work with the Sequence protocol all the time: arrays, dictionaries, strings, ranges and Data are all sequences. They come with a lot of convenient methods, like next(), contains(), filter() and more. Looping over a sequence uses its built-in iterator and stops when the iterator returns nil.
The AsyncSequence protocol works like Sequence, but an asynchronous sequence returns each element asynchronously (duh!). You can iterate over its elements asynchronously as more elements become available over time.
- You
awaiteach element, so the sequence can suspend while getting or calculating the next value. - The sequence might generate elements faster than your code can use them: One kind of
AsyncStreambuffers its values, so your app can read them when it needs them.
AsyncSequence provides language support for asynchronously processing collections of data. There are built-in AsyncSequences like NotificationCenter.Notifications, URLSession.bytes(from:delegate:) and its subsequences lines and characters. And you can create your own custom asynchronous sequences with AsyncSequence and AsyncIteratorProtocol or use AsyncStream.
Reading a File Asynchronously
For processing a dataset directly from a URL, the URL foundation class provides its own implementation of AsyncSequence in URL.lines. This is useful for creating an asynchronous sequence of lines directly from the URL.
Open ActorAPI.swift and add this method to ActorAPI:
// Asynchronous read
func readAsync() async throws {
let start = Date.now
var counter = 0
for try await _ in url.lines {
counter += 1
}
print("\(counter) lines")
print("Duration: \(Date.now.timeIntervalSince(start))")
}
You iterate asynchronously over the asynchronous sequence, counting lines as you go.
Here’s some Swift concurrency magic: url.lines has its own asynchronous iterator, and the for loop calls its next() method until the sequence signals it’s finished by returning nil.
URLSession has a method that gets an asynchronous sequence of bytes and the usual URLResponse object. You can check the response status code, then call lines on this sequence of bytes to convert it into an asynchronous sequence of lines.
let (stream, response) = try await URLSession.shared.bytes(from: url)
guard (response as? HTTPURLResponse)?.statusCode == 200 else {
throw "The server responded with an error."
}
for try await line in stream.lines {
// ...
}
let (stream, response) = try await URLSession.shared.bytes(from: url)
guard (response as? HTTPURLResponse)?.statusCode == 200 else {
throw "The server responded with an error."
}
for try await line in stream.lines {
// ...
}