Leave a rating/review
In this episode, you’ll learn more about async/await, then start exploring asynchronous sequences. You’ll need this to implement the live ticker view in the LittleJohn app.
Part 1: Asynchronous properties and subscripts
In the course materials, locate the starter playground and open it.
First, here are two more ways to use async/await.
Here are the data model and fetchDomains() function from episode 2.
And the Task that calls fetchDomains()
Async computed property
You can mark read-only computed properties with async and throws.
Find the TODO for Extend Domains with getter and add this extension:
extension Domains {
static var domains: [Domain] {
get async throws {
try await fetchDomains()
}
}
}
You’ve moved the call to fetchDomains() into the computed property’s getter.
Now, in Task, comment out let domains = try await fetchDomains() , and edit the for-in line:
for domain in try await Domains.domains {
You’re using the computed property. Because it’s asynchronous and can throw errors, you use try await.
Run the Task and you get the same results as before.
Async subscript
Now, scoll up to the second extension:
extension Domains {
enum Error: Swift.Error { case outOfRange }
static subscript(_ index: Int) -> String {
get async throws {
return ""
}
}
}
You can use your async computed property domains to create an asynchronous read-only subscript.
Replace this dummy return statement:
extension Domains {
enum Error: Swift.Error { case outOfRange }
static subscript(_ index: Int) -> String {
get async throws {
🟩
let domains = try await Self.domains
guard domains.indices.contains(index) else {
throw Error.outOfRange
}
return domains[index].attributes.name
🟥
}
}
}
You use domains to locate the requested element and throw an error if the requested index is out of range.
Now, create a Task above the existing Task to use this subscript:
Task {
dump(try await Domains[4])
}
Option-click dump to show its documentation.
You’re showing the contents of the 5th element.
You must try await because subscript uses domains, which is an async throwing computed property. Run this Task:
- "Game Tech"
It’s Game Tech. You might get a different domain. Sometimes new ones appear or old ones get renamed.
Click to the Next page.
Part 2: Asynchronous sequences
Another powerful abstraction that Swift concurrency gives you is the asynchronous sequence. It’s like the standard Swift sequence, except getting each element may cause the task to suspend.
Here’s an async function that iterates over an asynchronous sequence:
func findTitle(url: URL) async throws -> String? {
for try await line in url.lines {
if line.contains("<title>") {
return line.trimmingCharacters(in: .whitespaces)
}
}
return nil
}
You can iterate over an async sequence with a for-in loop, just like a non-async sequence, but you need the try await keywords.
The URL type has a lines property that returns an asynchronous sequence of strings — one string for each line in the file. Here, you’re expecting to receive an HTML file for a web page, and you’ll read each line until you find its title tag.
You loop over this sequence with for try await.
It can stop loading and return the answer as soon as it hits a line with the title tag in it.
Now, create a Task to test this function:
Task {
let url = URL(string: "https://www.raywenderlich.com")!
if let title = try await findTitle(url: url) {
print(title)
}
}
You’re reading through the raywenderlich.com home page, looking for the line with the <title> tag. Run this task:
<title>raywenderlich.com | High quality programming tutorials: iOS, Android, Swift, Kotlin, Flutter, Server Side Swift, Unity, and more!</title>
And here’s the title.
Sequence iterator
The for-in loop is really a while loop calling the next() method of an iterator for the sequence url.lines. You can create the iterator yourself, if you only want a fixed number of elements.
Add these lines to the Task:
var iterator = url.lines.makeAsyncIterator()
if let next = try await iterator.next() {
print("\n\(next)")
}
You create an iterator then print the next line, if there is one. Run the Task.
<title>raywenderlich.com | High quality programming tutorials: iOS, Android, Swift, Kotlin, Flutter, Server Side Swift, Unity, and more!</title>
<!DOCTYPE html>
And here’s the title and the next line.
Custom AsyncSequence
URL and some other built-in types have built-in async sequences, but how do you create your own async sequence? Take a closer look at AsyncSequence:
Option-click* lines, then click AsyncLineSequence to open its documentation. Click AsyncSequence.
The AsyncSequence protocol lets you create your own async sequences: Timer, NotificationCenter, UIDevice.orientation are a few examples. An asynchronous sequence lets you access its elements via its iterator.
Scroll down to Topics: The AsyncSequence protocol only requires you to define the element type of the sequence and provide an iterator. As you just saw with the lines example, the iterator also powers for-await-in loops.
The sequence’s iterator must conform to AsyncIteratorProtocol, which is also very minimal. Click it to see. Scroll down to Topics. The only requirements are the element type and an async method that returns the next element in the sequence.
To see how this works, you’ll create a simple typewriter — an asynchronous sequence that “types” a phrase, adding a character every second. Close the documentation window.
Scroll up to func findTitle and above it, add a new structure:
struct Typewriter: AsyncSequence {
}
Wait for Xcode to offer to add protocol stubs, then click Fix.
struct Typewriter: AsyncSequence {
typealias AsyncIterator = <#type#>
typealias Element = <#type#>
}
Set the AsyncIterator typealias to TypewriterIterator and the Element typealias to String:
struct Typewriter: AsyncSequence {
typealias AsyncIterator = 🟩TypewriterIterator🟥
typealias Element = 🟩String🟥
}
Below this structure, add a TypewriterIterator structure:
struct TypewriterIterator: AsyncIteratorProtocol {
typealias Element = String
mutating func next() async throws -> String? {
return ""
}
}
Xcode knows you need a next method and, because your Element type is String, the next() method knows it has to return an optional String. For now, just return an empty string.
Now that you have a placeholder iterator, let Xcode add a stub to Typewriter so it conforms to AsyncSequence — click Fix and for now, just return a default TypewriterIterator:
func makeAsyncIterator() -> TypewriterIterator {
return TypewriterIterator()
}
I like to have the typealias lines first, so I’ll move them up.
You’ve now got the bare bones of a custom AsyncSequence. It needs a phrase property.
Add the property, then pass it to TypewriterIterator:
struct Typewriter: AsyncSequence {
typealias Element = String
🟩let phrase: String🟥
func makeAsyncIterator() -> TypewriterIterator {
return TypewriterIterator(🟩phrase🟥)
}
}
And also add this property to your iterator and start using it:
struct TypewriterIterator: AsyncIteratorProtocol {
typealias Element = String
🟩let phrase: String
var index: String.Index
init(_ phrase: String) {
self.phrase = phrase
self.index = phrase.startIndex
}
🟥
mutating func next() async throws -> String? {
return ""
}
}
To step through the characters in phrase, you need to keep track of its index, so you initialize index to the start of phrase.
Now, replace the placeholder return in the next() method:
mutating func next() async throws -> String? {
🟩
guard index < phrase.endIndex else {
return nil
}
try await Task.sleep(until: .now + .seconds(1),
clock: .continuous)
defer {
index = phrase.index(after: index)
}
return String(phrase[phrase.startIndex...index])
🟥
}
First, check you haven’t reached the end of phrase.
Then introduce a 1-second delay before you return the substring.
Just before this method returns, increment index.
And finally, return the substring of phrase between its startIndex and the current index.
So, each time you call next(), it returns a substring of the initial string that is one character longer than the last one.
When it reaches the end of the phrase, either by a for await loop or some code that calls next() directly, next() returns nil to signify the end of the sequence.
Now, add a Task to try out your Typewriter sequence:
Task {
for try await item in Typewriter(phrase: "Hello, world!") {
print(item)
}
print("Done")
}
This is a sequence so you can iterate over it. The for-in loop uses this next() method of TypewriterIterator. Run this Task.
He
Hel
Hell
Hello
Hello,
Hello,
Hello, w
Hello, wo
Hello, wor
Hello, worl
Hello, world
Hello, world!
Done
So it’s pretty easy to create a custom AsyncSequence — you just have to add two extra types to your codebase. To avoid clutter, you can make a single type conform to both AsyncSequence and AsyncIteratorProtocol, but there’s also another, much easier, way. You’ll learn about AsyncStream in the next course Beyond the Basics.
Now, click to the Next page.
Part 3: Cancel a task
To finish this episode, learn how to cancel a task. Here are the two tasks from episode 2:
Task {
print("\nDoing some work on an unnamed task")
let sum = (1...100000).reduce(0, +)
print("Unnamed task done: 1 + 2 + 3 ... 100000 = \(sum)")
}
print("Doing some work on the main queue")
print("Doing more work on the main queue")
// This task runs after previous task finishes
let task = Task {
print("\nDoing some work on a named task")
// TODO: Check for cancellation before doing work
let sum = (1...100000).reduce(0, +)
print("Named task done: 1 + 2 + 3 ... 100000 = \(sum)")
}
print("Doing yet more work on the main queue")
And here’s why the second Task has a name: You need its name to cancel it.
Scroll down to the end of the playground and add these lines:
task.cancel()
print("\nCanceled task")
And run. Well, that didn’t change anything: The named task still computed sum.
Task cancellation is cooperative. The cancel() method only sets the isCancelled flag of the task. You need to check the task’s cancellation status before it does any expensive work.
Add a line before the sum calculation:
let task = Task {
print("\nDoing some work on a named task")
// Check for cancellation before doing work
🟩try Task.checkCancellation()🟥
let sum = (1... 100000).reduce(0, +)
print("Named task done: 1 + 2 + 3 ... 100000 = \(sum)")
}
print("Doing yet more work on the main queue")
task.cancel()
If you call cancel() before task begins to run, checkCancellation() throws a CancellationError. Run this code.
Doing some work on an unnamed task
Doing some work on the main actor
Doing more work on the main actor
Doing yet more work on the main queue
Canceled task
Unnamed task done: 1 + 2 + 3 ... 100000 = 5000050000
Doing some work on a named task
The unnamed Task delays the start of task, so task.cancel() happens before task starts, and so task exits before it calculates sum:
If you want more control over what happens when a task is cancelled, use Task.isCancelled.
Comment out try Task.checkCancellation() , then add this code:
if Task.isCancelled {
print("Task canceled")
throw CancellationError()
}
You print a message, then manually throw CancellationError. Run this code.
There’s your message, and the named task exits without calculating sum.
In the next episode, you’ll see another way to check for cancellation. The URLSession API throws custom errors and has a dedicated cancellation error code.