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.

4.6 (16) · 1 Review

Download materials
Save for later
Share
You are currently viewing page 4 of 5 of this article. Click here to view the first page.

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:

Publishing changes from background threads is not allowed.

Publishing changes from background threads is not allowed.

Add breakpoints inside load() and in fetchJoke() at isFetching = true, let loadedJoke = ... and joke = loadedJoke.value:

Set breakpoints.

Set breakpoints.

Tap the button again, then watch the threads while you click Continue program execution after each breakpoint:

fetchJoke() starts on the main thread but moves to background thread.

fetchJoke() starts on the main thread but moves to background thread.

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.

Note: If you mark a class as @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:

fetchJoke() runs on the main thread after load() finishes.

fetchJoke() runs on the main thread after load() finishes.

The first three breakpoints are the same as before, but now fetchJoke() runs on the main thread after load() finishes.

@MainActor fetchJoke() runs on main thread.

@MainActor fetchJoke() runs on main thread.

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()
  }
}
  1. This view is a List because refreshable(action:) only works with scrollable views.
  2. The task modifier performs its action when the view appears. Its action parameter’s type is @escaping () async -> Void. It creates a task to run the action so you don’t need to.
  3. The refreshable modifier’s action 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:

A joke appears when the view loads.

A joke appears when the view loads.
Note: Actually, this algorithm is 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.

WWDC 2021 Videos

To learn more about the fundamental concepts of Swift concurrency, watch these videos first:

  • Meet async/await in Swift: This session introduces try await, get async, unstructured task and continuations.
  • Explore structured concurrency in Swift: This session covers concurrent and sequential binding, cancellation, task groups, unstructured and detached tasks. There’s a handy Flavors of task table near the end.
  • Protect mutable state with Swift actors: This session covers data races, actor, protocol conformance in extensions with nonisolated declarations, detached tasks, Sendable, conformance and MainActor.

These sessions are SwiftUI-specific:

  • Demystify SwiftUI: Get inspired to check all your SwiftUI code for inefficiencies. Lots of cute dog and cat photos.
  • Discover concurrency in SwiftUI: This session provides valuable tips for using structured concurrency in a SwiftUI app (SpacePhoto).

These talks cover a few specific purposes:

Set aside a good chunk of uninterrupted time for these deeper dives, or watch them in 10-15 minute chunks: