async/await in SwiftUI
Convert a SwiftUI app to use the new Swift concurrency and find out what’s going on beneath the shiny surface. 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
async/await in SwiftUI
35 mins
- Getting Started
- Old and New Concurrency
- Pyramid of Doom
- Data Races
- Thread Explosion / Starvation
- Tasks and Continuations
- JokeService
- Minimal Error Handling
- Show Me a Joke!
- Concurrent Binding
- Awaiting async
- Sequential Binding
- Error Handling Options
- Creating an Unstructured Task
- Decoding the Joke
- MainActor
- Actor
- @MainActor
- One More Thing: Asynchronous View Modifiers
- Where to Go From Here?
- WWDC 2021 Videos
- Melbourne Cocoaheads
Swift 5.5 has a shiny new structured concurrency framework to help you write safe code more swiftly. To help everyone get started, Apple provided a bunch of videos and sample code at WWDC 2021. There’s a quick run-down on what they cover at the end of this tutorial.
The Twitterverse exploded and the usual actors (;]) have already published several how-tos. This tutorial is like a micro version of Swift concurrency: Update a sample app from WWDC. You’ll take baby steps to convert a much simpler app to learn how async/await and actors help you write safer code. To help you decipher Xcode’s error messages and future-proof you against the inevitable future API changes, you’ll explore what’s going on beneath the shiny surface.
Getting Started
Create a new Xcode project that uses SwiftUI interface and name it WaitForIt.
In ContentView.swift, replace the body
contents with this code:
AsyncImage(url: URL(string: "https://files.betamax.raywenderlich.com/attachments/collections/194/e12e2e16-8e69-432c-9956-b0e40eb76660.png")) { image in
image.resizable()
} placeholder: {
Color.red
}
.frame(width: 128, height: 128)
In Xcode 13 beta 1, you get this error:
Don’t click any of the Fix buttons! Go to the target page and change Deployment Info from iOS 14.0 to iOS 15.0:
Go back to ContentView.swift. If the error message is still there, press Command-B to build the project.
Run Live Preview to see the image for the “SwiftUI vs. UIKit” video:
OK, that was just a quick check to fix that Xcode glitch and also to show you SwiftUI’s new AsyncImage
view. Good, isn’t it? :]
Before you get to work on the real WaitForIt app, take a high level look at how the new Swift concurrency fixes problems with the old GCD concurrency.
Old and New Concurrency
The old GCD concurrency has several problems that make it hard to write apps that safely use concurrency.
Swift concurrency provides the necessary tools to carve work up into smaller tasks that can run concurrently. This lets tasks wait for each other to complete and allows you to effectively manage the overall progress of a task.
Pyramid of Doom
Swift APIs like URLSession are asynchronous. Methods automatically dispatch to a background queue and immediately return control to the calling code. Methods take a completion handler and call delegate methods. Completion or delegate code that accesses UI elements must be dispatched to the main queue.
If a completion handler calls another asynchronous function, and this function has a completion handler, it’s hard to see the happy path in the resulting pyramid of doom. This makes it hard to check the code is correct. For example, this sample code from WWDC’s Meet async/await in Swift downloads data, creates an image from the data, then renders a thumbnail from the image. Error handling is ad hoc because completion handlers can’t throw errors.
func fetchThumbnail(
for id: String,
completion: @escaping (UIImage?, Error?) -> Void
) {
let request = thumbnailURLRequest(for: id)
let task = URLSession.shared
.dataTask(with: request) { data, response, error in
if let error = error {
completion(nil, error)
} else if (response as? HTTPURLResponse)?.statusCode != 200 {
completion(nil, FetchError.badID)
} else {
guard let image = UIImage(data: data!) else {
completion(nil, FetchError.badImage)
return
}
image.prepareThumbnail(of: CGSize(width: 40, height: 40)) { thumbnail in
guard let thumbnail = thumbnail else {
completion(nil, FetchError.badImage)
return
}
completion(thumbnail, nil)
}
}
}
task.resume()
}
The sequence of operations is much easier to see with async/await, and you can take advantage of Swift’s robust error handling mechanism:
func fetchThumbnail(for id: String) async throws -> UIImage {
let request = thumbnailURLRequest(for: id)
let (data, response) = try await URLSession.shared.data(for: request)
guard (response as? HTTPURLResponse)?.statusCode == 200 else {
throw FetchError.badID
}
let maybeImage = UIImage(data: data)
guard let thumbnail = await maybeImage?.thumbnail else {
throw FetchError.badImage
}
return thumbnail
}
Data Races
When multiple tasks can read or write an object’s data, data races are possible. A data race occurs when one task sleeps while another task writes and exits, then the sleeping task resumes and overwrites what the previous task wrote. This creates inconsistent results.
In an app using the old concurrency, Xcode can detect data races if you enable the runtime Thread Sanitizer diagnostic in your app’s Run scheme. Then you can implement a serial queue to prevent concurrent access.
The new Swift concurrency model provides the Actor
protocol to prevent concurrent access to an object’s data. Actors also enable you to structure your app into code that runs on the main thread and code that runs on background threads, so the compiler can help you prevent concurrent access.
Thread Explosion / Starvation
In GCD, the main unit of work is a thread. If your code queues up a lot of read/write tasks on a serial queue, most of them must sleep while they wait. This means their threads are blocked, so the system creates more threads for the next tasks. If each task also queues a completion handler on another queue, that creates even more threads. Every blocked thread holds onto a stack and kernel data structures so it can resume. A blocked thread may be holding resources that another thread needs, so that thread blocks.
This is a thread explosion: The system is overcommitted with many times more threads than it has cores to process them. The scheduler must allocate time to hundreds of threads, resulting in a lot of context switching. All of this slows down your app and can even starve some threads, so they never make any progress.