In this post, we are going to showcase some common examples of working with async await and update a SwiftUI view. Let’s consider a scenario where we need to perform asynchronous work when the view appears, such as loading an image. After that, we will update the SwiftUI view.
.task modifier
For this purpose, we have the perform modifier for the job, the .task
modifier:
Swiftstruct ContentView: View {
@State var data: Data?
var body: some View {
VStack {
if let data = data,
let image = UIImage(data: data) {
Image(uiImage: image)
.resizable()
}
}
.task {
do {
let url = URL(string: "https://developer.apple.com/wwdc23/topics/images/og/swift-og.png")!
data = try await URLSession.shared.data(from: url).0
} catch {
//Handle error
}
}
}
}
Since this modifier creates its own asynchronous context, we can perform any asynchronous block. Additionally, it will handle cancellation for us, so as soon as the view is dismissed, the task will be canceled.
But what about if we need to perform some asynchronous work inside a closure, such as after tapping a button? We cannot use the modifier within the closure. To perform asynchronous work in such cases, we need to create an asynchronous context, and for that, we can use the Task
type:
Swiftstruct ContentView: View {
@State var data: Data?
var body: some View {
VStack {
if let data = data,
let image = UIImage(data: data) {
Image(uiImage: image)
.resizable()
}
Button(action: {
Task {
do {
let url = URL(string: "https://developer.apple.com/wwdc23/topics/images/og/swift-og.png")!
data = try await URLSession.shared.data(from: url).0
} catch {
//Handle error
}
}
}, label: {
Text("Load image")
})
}
}
}
In this case, cancellation is not handled automatically, so it’s up to us to handle it. We explore how to handle cancellation in this article.
Performing updates
Just like in UIKit, we need to be cautious when performing UI updates on the main thread. When we create a new asynchronous context, this context inherits the priority from the current one. So when we do this inside a SwiftUI view, which is executed by the main actor, our asynchronous context starts on the main thread, allowing us to perform UI state changes initially. However, after any await
instruction, we have no guarantee about the thread we’re on. Fortunately, if you try any of the previous examples, everything works fine because when we update a @State
variable, SwiftUI is smart enough to understand that it needs to be done on the main thread. Let’s try a different but common approach using an @Published
property instead of an @State
variable.
A common example involves having an observable model that can perform asynchronous work and update a @Published
property, which is observed by the view
Swiftfinal class ContentObservableModel: ObservableObject {
@Published var data: Data?
func loadImage() async throws {
let url = URL(string: "https://developer.apple.com/wwdc23/topics/images/og/swift-og.png")!
data = try await URLSession.shared.data(from: url).0
}
}
struct ContentView: View {
@ObservedObject var model: ContentObservableModel
var body: some View {
VStack {
if let data = model.data,
let image = UIImage(data: data) {
Image(uiImage: image)
.resizable()
}
Button(action: {
Task {
do {
try await model.loadImage()
} catch {
//Handle error
}
}
}, label: {
Text("Load image")
})
}
.padding()
}
}
If we execute the previous code, we will receive an error related to updating the UI from a background thread:
Publishing changes from background threads is not allowed; make sure to publish values from the main thread (via operators like receive(on:)) on model updates.
Update SwiftUI view on the main thread when using Async Await
We need to ensure that the property is updated on the main thread. There are different ways to ensure this.
One approach is to use MainActor.run
, where whatever we do in the closure is executed on the main actor:
Swiftfinal class ContentObservableModel: ObservableObject {
@Published var data: Data?
func loadImage() async throws {
let url = URL(string: "https://developer.apple.com/wwdc23/topics/images/og/swift-og.png")!
let result = try await URLSession.shared.data(from: url).0
await MainActor.run {
data = result
}
}
}
By using the @MainActor
property wrapper, which can be added to any function, updating the UI becomes a straightforward task
Swiftfinal class ContentObservableModel: ObservableObject {
@MainActor @Published var data: Data?
func loadImage() async throws {
let url = URL(string: "https://developer.apple.com/wwdc23/topics/images/og/swift-og.png")!
let result = try await URLSession.shared.data(from: url).0
await updateData(result)
}
@MainActor func updateData(_ newData: Data) async {
data = newData
}
}
Conclusion
We’ve explored various options for initiating asynchronous work from a SwiftUI view and noted that the same rules for updating the UI from the main thread still apply to SwiftUI. In my next article, I want to focus on how to handle cancellation properly. See you then!