WWDC Notes: Discover concurrency in SwiftUI
Published on: June 9, 2021When 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.