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
Actor
Actors enable you to structure your app into actors on background threads and actors on the main thread, just as you now create model, view and view model files. Code in an actor
(lower case, not MainActor
) runs on a background thread. So you just need to move the asynchronous part of fetchJoke()
into a separate actor
.
In JokeService.swift, delete the breakpoint and add this code above JokeService
:
private actor JokeServiceStore {
private var loadedJoke = Joke(value: "")
func load() async throws -> Joke {
}
}
You create an actor
with a Joke
variable and initialize it with an empty String
, then write the stub of load()
, where you’ll move the download code. This method resets loadedJoke
and also returns the Joke
, so you don’t really need a Joke
property for this simple example, but you probably will for more complex data.
Next, create a JokeServiceStore
object in JokeService
(in the class, not the extension):
private let store = JokeServiceStore()
Now move the url
code from JokeService
into JokeServiceStore
:
private var url: URL {
urlComponents.url!
}
private var urlComponents: URLComponents {
var components = URLComponents()
components.scheme = "https"
components.host = "api.chucknorris.io"
components.path = "/jokes/random"
components.setQueryItems(with: ["category": "dev"])
return components
}
Then move the download code from fetchJoke()
to load()
, leaving only the two isFetching
lines in fetchJoke()
:
// move this code from fetchJoke() to load()
let (data, response) = try await URLSession.shared.data(from: url)
guard
let httpResponse = response as? HTTPURLResponse,
httpResponse.statusCode == 200
else {
throw DownloadError.statusNotOk
}
guard let decodedResponse = try? JSONDecoder().decode(Joke.self, from: data)
else { throw DownloadError.decoderError }
joke = decodedResponse.value
JokeServiceStore
has a Joke
property, not a String
property, so replace the last line with this code:
loadedJoke = decodedResponse
return loadedJoke
Instead of extracting just the value
from decodedResponse
, you set the Joke
property and also return this Joke
instance.
Now call load()
in fetchJoke()
:
let loadedJoke = try await store.load()
joke = loadedJoke.value
Build and run. Tap the button.
A joke appears, but you have purple warnings:
Add breakpoints inside load()
and in fetchJoke()
at isFetching = true
, let loadedJoke = ...
and joke = loadedJoke.value
:
Tap the button again, then watch the threads while you click Continue program execution after each breakpoint:
The first two lines of fetchJoke()
run on the main thread because a view calls it. Then load()
runs on a background thread, as it should. But when execution returns to fetchJoke()
, it’s still on a background thread. You need to do something to make it run on the main thread.
@MainActor
Code that sets a Published
value has to run on the MainActor
thread. When fetchJoke()
did all the work, and you called it from Button
in an unstructured task, fetchJoke()
inherited MainActor
from Button
, and all of its code ran on the MainActor
thread.
Now fetchJoke()
calls load()
, which runs on a background thread. fetchJoke()
still starts on the main thread but, when load()
finishes, fetchJoke()
continues running on a background thread.
fetchJoke()
doesn’t have to rely on inheriting MainActor
from Button
. You can mark a class or a function with the @MainActor
attribute to say that it must be executed on the MainActor
thread.
@MainActor
, any calls from outside MainActor
must await
, even when calling a method that completes its work immediately. A method that doesn’t reference any mutable state can opt out of MainActor
with the keyword nonisolated
.
Add this line above func fetchJoke() throws {
@MainActor
Build and run again and click through the breakpoints:
The first three breakpoints are the same as before, but now fetchJoke()
runs on the main thread after load()
finishes.
When fetchJoke()
calls load()
, it suspends itself, releasing the main thread to run UI jobs. When load finishes, fetchJoke()
again runs on the main thread, where it’s allowed to set the Published
values.
Your work here is done! Try converting your own SwiftUI projects: Take it slow, make small changes and try to keep the app buildable after each change.
One More Thing: Asynchronous View Modifiers
SwiftUI now has (at least) two view modifiers that expect their action
to call an asynchronous function.
Create a new SwiftUI View file named RefreshableView.swift and replace the contents of RefreshableView
with this:
@StateObject var jokeService = JokeService()
var body: some View {
List {
Text("Chuck Norris Joke")
.font(.largeTitle)
.listRowSeparator(.hidden)
Text(jokeService.joke)
.multilineTextAlignment(.center)
.lineLimit(nil)
.lineSpacing(5.0)
.padding()
.font(.title)
}
.task {
try? await jokeService.fetchJoke()
}
.refreshable {
try? await jokeService.fetchJoke()
}
}
- This view is a
List
becauserefreshable(action:)
only works with scrollable views. - The
task
modifier performs its action when the view appears. Itsaction
parameter’s type is@escaping () async -> Void
. It creates a task to run the action so you don’t need to. - The
refreshable
modifier’saction
parameter type is the same. It must be asynchronous. When applied to a scrollable view, the user can pull down to refresh its content, and it displays a refresh indicator until the asynchronous task completes.
Run Live Preview. A joke appears:
O(N)
. ;]
Pull down to fetch another joke. You might have to pull down quite far.
If you want to run this version in a simulator or on a device, open WaitForItApp.swift and change ContentView()
to RefreshableView()
in the WindowGroup
closure.
Where to Go From Here?
Download the final project using the Download Materials button at the top or bottom of the tutorial.
In this tutorial, you converted a simple SwiftUI app from the old GCD concurrency to the new Swift concurrency, using async/await, throwing, an unstructured task, actor
and @MainActor
.