Dispatching to the Main thread with MainActor in Swift

Published on: April 23, 2024

Swift 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:

  1. Apply an @MainActor annotation to both homePageData and loadHomePage
  2. Apply @MainActor to the entire HomePageViewModel to isolate both the homePageData property and the loadHomePage function to the main actor
  3. Use MainActor.run or an unstructured task that’s isolated to the main actor inside of loadHomePage.

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.

Subscribe to my newsletter