Dispatching to the Main thread with MainActor in Swift
Published on: April 23, 2024Swift 5.5 introduced loads of new concurrency related features. One of these features is the MainActor
annotation that we can apply to classes, functions, and properties.
In this post you’ll learn several techniques that you can use to dispatch your code to the main thread from within Swift Concurrency’s tasks or by applying the main actor annotation.
If you’d like to take a deep dive into learning how you can figure out whether your code runs on the main actor I highly recommend reading this post which explores Swift Concurrency’s isolation features.
Alternatively, if you’re interested in a deep dive into Swift Concurrency and actors I highly recommend that you check out my book on Swift Concurrency or that you check out my video course on Swift Concurrency. Both of these resources will give you deeper insights and background information on actors.
Dispatching to the main thread through the MainActor annotation
The quickest way to get a function to run on the main thread in Swift Concurrency is to apply the @MainActor
annotation to it:
class HomePageViewModel: ObservableObject {
@Published var homePageData: HomePageData?
@MainActor
func loadHomePage() async throws {
self.homePageData = try await networking.fetchHomePage()
}
}
The code above will run your loadHomePage
function on the main thread. The cool thing about this is that the await
in this function isn’t blocking the main thread. Instead, it allows our function to be suspended so that the main thread can do some other work while we wait for fetchHomePage()
to come back with some data.
The effect that applying @MainActor
to this function has is that the assignment of self.homePageData
happens on the main thread which is good because it’s an @Published
property so we should always assign to it from the main thread to avoid main thread related warnings from SwiftUI at runtime.
If you don’t like the idea of having all of loadHomePage
run on the main actor, you can also annotate the homePageData
property instead:
class HomePageViewModel: ObservableObject {
@MainActor @Published var homePageData: HomePageData?
func loadHomePage() async throws {
self.homePageData = try await networking.fetchHomePage()
}
}
Unfortunately, this code leads to the following compiler error:
Main actor-isolated property 'homePageData' can not be mutated from a non-isolated context
This tells us that we’re trying to mutate a property, homePageData
on the main actor while our loadHomePage
method is not running on the main actor which is data safety problem in Swift Concurrency; we must mutate the homePageData
property from a context that’s isolated to the main actor.
We can solve this issue in one of three ways:
- Apply an
@MainActor
annotation to bothhomePageData
andloadHomePage
- Apply
@MainActor
to the entireHomePageViewModel
to isolate both thehomePageData
property and theloadHomePage
function to the main actor - Use
MainActor.run
or an unstructured task that’s isolated to the main actor inside ofloadHomePage
.
The quickest fix is to annotate our entire class with @MainActor
to run everything that our view model does on the main actor:
@MainActor
class HomePageViewModel: ObservableObject {
@Published var homePageData: HomePageData?
func loadHomePage() async throws {
self.homePageData = try await networking.fetchHomePage()
}
}
This is perfectly fine and will make sure that all of your view model work is performed on the main actor. This is actually really close to how your view model would work if you didn’t use Swift Concurrency since you normally call all view model methods and properties from within your view anyway.
Let’s see how we can leverage option three from the list above next.
Dispatching to the main thread with MainActor.run
If you don’t want to annotate your entire view model with the main actor, you can isolate chunks of your code to the main actor by calling the static run
method on the MainActor
object:
class HomePageViewModel: ObservableObject {
@Published var homePageData: HomePageData?
func loadHomePage() async throws {
let data = try await networking.fetchHomePage()
await MainActor.run {
self.homePageData = data
}
}
}
Note that the closure that you pass to run
is not marked as async
. This means that any asynchronous work that you want to do needs to happen before your call to MainActor.run
. All of the work that you put inside of the closure that you pass to MainActor.run
is executed on the main thread which can be quite convenient if you don’t want to annotate your entire loadHomePage
method with @MainActor
.
The last method to dispatch to main that I’d like to show is through an unstructured task.
Isolating an unstructured task to the main actor
if you’re creating a new Task
and you want to make sure that your task runs on the main actor, you can apply an @MainActor
annotation to your task’s body as follows:
class HomePageViewModel: ObservableObject {
@Published var homePageData: HomePageData?
func loadHomePage() async throws {
Task { @MainActor in
self.homePageData = try await networking.fetchHomePage()
}
}
}
In this case, we should have just annotated our loadHomePage
method with @MainActor
because we’re creating an unstructured task that we don’t need and we isolate our task to main.
However, if you’d have to write loadHomePage
as a non-async method creating a new main-actor isolated task can be quite useful.
In Summary
In this post you’ve seen several ways to dispatch your code to the main actor using @MainActor
and MainActor.run
. The main actor is intended to replace your calls to DispatchQueue.main.async
and with this post you have all the code examples you need to be able to do just that.
Note that some of the examples provided in this post produce warnings under strict concurrency checking. That’s because the HomePageViewModel
I’m using in this post isn’t Sendable
. Making it conform to Sendable
would get rid of all warnings so it’s a good idea to brush up on your knowledge of Sendability if you’re keen on getting your codebase ready for Swift 6.