NEW: Learn to build the incredible iOS 15 Weather app today! >>

How to use @MainActor to run code on the main queue

Paul Hudson    @twostraws   

Updated for Xcode 13.2

@MainActor is a global actor that uses the main queue for executing its work. In practice, this means any method or type marked with @MainActor can safely modify the UI because it will always be running on the main queue, and calling MainActor.run() will push some custom work of your choosing to the main actor, and thus to the main queue. At the simplest level both of these features are straightforward to use, but as you’ll see there’s a lot of complexity behind them.

First, let’s look at using @MainActor, which automatically makes a single method or all methods on a type run on the main actor. This is particularly useful for any types that exist to update your user interface, such as ObservableObject classes.

For example, we could create a observable object with two @Published properties, and because they will both update the UI we would mark the whole class with @MainActor to ensure these UI updates always happen on the main actor:

@MainActor
class AccountViewModel: ObservableObject {
    @Published var username = "Anonymous"
    @Published var isAuthenticated = false
}

In fact, this set up is so central to the way ObservableObject works that SwiftUI bakes it right in: whenever you use @StateObject or @ObservedObject inside a view, Swift will ensure that the whole view runs on the main actor so that you can’t accidentally try to publish UI updates in a dangerous way. Even better, no matter what property wrappers you use, the body property of your SwiftUI views is always run on the main actor.

Does that mean you don’t need to explicitly add @MainActor to observable objects? Well, no – there are still benefits to using @MainActor with these classes, not least if they are using async/await to do their own asynchronous work such as downloading data from a server.

So, my recommendation is simple: even though SwiftUI ensures main-actor-ness when using @ObservableObject, @StateObject, and SwiftUI view body properties, it’s a good idea to add the @MainActor attribute to all your observable object classes to be absolutely sure all UI updates happen on the main actor. If you need certain methods or computed properties to opt out of running on the main actor, use nonisolated as you would do with a regular actor.

Important: I’ve said it previously, but it’s worth repeating: you should not attempt to use actors for your observable objects, because they must do their UI updates on the main actor rather than a custom actor.

More broadly, any type that has @MainActor objects as properties will also implicitly be @MainActor using global actor inference – a set of rules that Swift applies to make sure global-actor-ness works without getting in the way too much. I’ll cover these rules in the next chapter, because they are quite precise.

The magic of @MainActor is that it automatically forces methods or whole types to run on the main actor, without any further work from us. Previously we needed to do it by hand, remembering to use code like DispatchQueue.main.async() or similar every place it was needed, but now the compiler does it for us automatically.

If you do need to spontaneously run some code on the main actor, you can do that by calling MainActor.run() and providing your work. This allows you to safely push work onto the main actor no matter where your code is currently running, like this:

func couldBeAnywhere() async {
    await MainActor.run {
        print("This is on the main actor.")
    }
}

await couldBeAnywhere()

Download this as an Xcode project

You can send back nothing from run() if you want, or send back a value like this:

func couldBeAnywhere() async {
    let result = await MainActor.run { () -> Int in
        print("This is on the main actor.")
        return 42
    }

    print(result)
}

await couldBeAnywhere()

Download this as an Xcode project

. Even better, if that code was already running on the main actor then the code is executed immediately – it won’t wait until the next run loop in the same way that DispatchQueue.main.async() would have done.

If you wanted the work to be sent off to the main actor without waiting for its result to come back, you can place it in a new task like this:

func couldBeAnywhere() async {
    Task.detached {
        await MainActor.run {
            print("This is on the main actor.")
        }
    }
}

await couldBeAnywhere()

Download this as an Xcode project

Tip: I used Task.detached() here to ensure we do not inherit any existing actor context.

Although it’s possible to create your own global actors, I think we should probably avoid doing so until we’ve had sufficient chance to build apps using what we already have.

Hacking with Swift is sponsored by Essential Developer

SPONSORED Learn the most up-to-date techniques and strategies for testing new and legacy Swift code in this free practical course for iOS devs who want to become complete Senior iOS Developers.

Learn more

Sponsor Hacking with Swift and reach the world's largest Swift community!

Similar solutions…

BUY OUR BOOKS
Buy Pro Swift Buy Swift Design Patterns Buy Testing Swift Buy Hacking with iOS Buy Swift Coding Challenges Buy Swift on Sundays Volume One Buy Server-Side Swift (Vapor Edition) Buy Advanced iOS Volume One Buy Advanced iOS Volume Two Buy Advanced iOS Volume Three Buy Hacking with watchOS Buy Hacking with tvOS Buy Hacking with macOS Buy Dive Into SpriteKit Buy Swift in Sixty Seconds Buy Objective-C for Swift Developers Buy Server-Side Swift (Kitura Edition) Buy Beyond Code

Was this page useful? Let us know!

 
Unknown user

You are not logged in

Log in or create account
 

Link copied to your pasteboard.