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
AsyncStream
s.
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
await
each 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
AsyncStream
buffers 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 AsyncSequence
s 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 {
// ...
}