WWDC Notes: Discover concurrency in SwiftUI

Published on: June 9, 2021

When performing slow work, you might dispatch off of the main queue. Updating an observable object off of the main queue could result in this updating colliding with a “tick” of the run loop. This means that SwiftUI receive an objectWillChange, and attempt to redraw UI before the underlying value is updated.

This will lead to SwiftUI thinking that your model is in one state, but it’s in the next.

SwiftUI needs to have objectWillChange->stateChange->runloop tick in this exact order.

Running your update on the main actor (or main queue pre async/await) will ensure that the state change is completed before the runloop tick since the operation would be atomic.

You can use await to ensure this. Doing this is called yielding (to) the main actor.

When you’re on the main actor and you call a function with await, you yield the actor, allowing it to do other work. The work is then performed by a different actor. When this work completes, control is handed back to the main actor where it will update state:

class Photos: ObservableObject {
  @Pulished var items: [SpacePhoto] = []

  func update() async {
    let fetched = await fetch() // yields main actor
    items = fetched // done on the main actor
  }
}

There’s currently guarantee that item is always accessed on the main actor. To make this guarantee, class Photos needs the @MainActor annotation.

The task modifier on a SwiftUI is used to run an async task on creation. It’s called at the same point in the lifecycle as onAppear.

Since task is tied to the view’s lifecycle, you can await an async sequence’s elements in task and rest assured that everything is cancelled and cleaned up when the view’s lifecycle ends.

Button methods in SwiftUI are synchronous. To launch an async task from a button handler, use async {} (will be renamed to Task.init) and await your async work.

Button("Save") {
  async {
    isSaving = true
    await model.save()
    isSaving = false
  }
}

In this button isSaving is mutated on the main actor. async (or Task.init) runs its task attached to the current actor. In a SwiftUI view, this would be the main actor. await will yield the main actor and run code on whatever actor model.save() runs until control is yielded back to the main actor.

The .refreshable modifier on SwiftUI takes an async closure. You can await an update operation in there. This modifier will, by default, use a pull to refresh control.

SwiftUI integrates nicely with async / await and asynchronous functions.

It’s recommended to mark ObservableObject with @MainActor to ensure that their property access and mutations are done savely on the main actor.

Categories

WWDC21 Notes

Subscribe to my newsletter