Today I refactored a ViewModel for a SwiftUI view to structured concurrency. It fires a network request and when the request comes back, updates a @Published
property to update the UI. Since I use a Task
to perform the network request, I have to get back to the MainActor
to update my property, and I was exploring different ways to do that. One straightforward way was to use MainActor.run
inside my Task
, which works just fine. I then tried to use @MainActor
, and don't quite understand the behaviour here.
A bit simplified, my ViewModel would look somewhat like this:
class ContentViewModel: ObservableObject {
@Published var showLoadingIndicator = false
@MainActor func reload() {
showLoadingIndicator = true
Task {
try await doNetworkRequest()
showLoadingIndicator = false
}
}
@MainActor func someOtherMethod() {
// does UI work
}
}
I would have expected this to not work properly.
First, I expected SwiftUI to complain that showLoadingIndicator = false
happens off the main thread. It didn't. So I put in a breakpoint, and it seems even the Task
within a @MainActor
is run on the main thread. Why that is is maybe a question for another day, I think I haven't quite figured out Task
yet. For now, let's accept this.
So then I would have expected the UI to be blocked during my networkRequest - after all, it is run on the main thread. But this is not the case either. The network request runs, and the UI stays responsive during that. Even a call to another method on the main actor (e.g. someOtherMethod
) works completely fine.
Even running something like Task.sleep()
within doNetworkRequest
will STILL work completely fine. This is great, but I would like to understand why.
My questions:
a) Am I right in assuming a Task
within a MainActor
does not block the UI? Why?
b) Is this a sensible approach, or can I run into trouble by using @MainActor
for dispatching asynchronous work like this?