UPGRADE YOUR SKILLS: Learn advanced Swift and SwiftUI on Hacking with Swift+! >>

Understanding how global actor inference works

Paul Hudson    @twostraws   

Updated for Xcode 15

Apple explicitly annotates many of its types as being @MainActor, including most UIKit types such as UIView and UIButton. However, there are many places where types gain main-actor-ness implicitly through a process called global actor inference – Swift applies @MainActor automatically based on a set of predetermined rules.

There are five rules for global actor inference, and I want to tackle them individually because although they start easy they get more complex.

First, if a class is marked @MainActor, all its subclasses are also automatically @MainActor. This follows the principle of least surprise: if you inherit from a @MainActor class it makes sense that your subclass is also @MainActor.

Second, if a method in a class is marked @MainActor, any overrides for that method are also automatically @MainActor. Again, this is a natural thing to expect – you overrode a @MainActor method, so the only safe way Swift can call that override is if it’s also @MainActor.

Third, any struct or class using a property wrapper with @MainActor for its wrapped value will automatically be @MainActor. This is what makes @StateObject and @ObservedObject convey main-actor-ness on SwiftUI views that use them – if you use either of those two property wrappers in a SwiftUI view, the whole view becomes @MainActor too. At the time of writing Xcode’s generated interface for those two property wrappers don’t show that they are annotated as @MainActor, but I’ve been assured they definitely are – hopefully Xcode can make that work better in the future.

The fourth rule is where the difficulty ramps up a little: if a protocol declares a method as being @MainActor, any type that conforms to that protocol will have that same method automatically be @MainActor unless you separate the conformance from the method.

What this means is that if you make a type conform to a protocol with a @MainActor method, and add the required method implementation at the same time, it is implicitly @MainActor. However, if you separate the conformance and the method implementation, you need to add @MainActor by hand.

Here’s that in code:

// A protocol with a single `@MainActor` method.
protocol DataStoring {
    @MainActor func save()
}

// A struct that does not conform to the protocol.
struct DataStore1 { }

// When we make it conform and add save() at the same time, our method is implicitly @MainActor.
extension DataStore1: DataStoring {
    func save() { } // This is automatically @MainActor.
}

// A struct that conforms to the protocol.
struct DataStore2: DataStoring { }

// If we later add the save() method, it will *not* be implicitly @MainActor so we need to mark it as such ourselves.
extension DataStore2 {
    @MainActor func save() { }
}

As you can see, we need to explicitly use @MainActor func save() in DataStore2 because the global actor inference does not apply there. Don’t worry about forgetting it, though – Xcode will automatically check the annotation is there, and offer to add @MainActor if it’s missing.

The fifth and final rule is most complex of all: if the whole protocol is marked with @MainActor, any type that conforms to that protocol will also automatically be @MainActor unless you put the conformance separately from the main type declaration, in which case only the methods are @MainActor.

In attempt to make this clear, here’s what I mean:

// A protocol marked as @MainActor.
@MainActor protocol DataStoring {
    func save()
}

// A struct that conforms to DataStoring as part of its primary type definition.
struct DataStore1: DataStoring { // This struct is automatically @MainActor.
    func save() { } // This method is automatically @MainActor.
}

// Another struct that conforms to DataStoring as part of its primary type definition.
struct DataStore2: DataStoring { } // This struct is automatically @MainActor.

// The method is provided in an extension, but it's the same as if it were in the primary type definition.
extension DataStore2 {
    func save() { } // This method is automatically @MainActor.
}

// A third struct that does *not* conform to DataStoring in its primary type definition.
struct DataStore3 { } // This struct is not @MainActor.

// The conformance is added as an extension
extension DataStore3: DataStoring {
    func save() { } // This method is automatically @MainActor.
}

I realize that might sound obscure, but it makes sense if you put it into a real-world context. For example, let’s say you were working with the DataStoring protocol we defined above – what would happen if you modified one of Apple’s types so that it conformed to it?

If conformance to a @MainActor protocol retroactively made the whole of Apple’s type @MainActor then you would have dramatically altered the way it worked, probably breaking all sorts of assumptions made elsewhere in the system. If it’s your type – a type you’re creating from scratch in your own code – then you can add the protocol conformance as you make the type and therefore isolate the entire type to @MainActor, because it’s your choice.

BUILD THE ULTIMATE PORTFOLIO APP Most Swift tutorials help you solve one specific problem, but in my Ultimate Portfolio App series I show you how to get all the best practices into a single app: architecture, testing, performance, accessibility, localization, project organization, and so much more, all while building a SwiftUI app that works on iOS, macOS and watchOS.

Get it on Hacking with Swift+

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

Similar solutions…

BUY OUR BOOKS
Buy Pro Swift Buy Pro SwiftUI 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 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 Beyond Code

Was this page useful? Let us know!

Average rating: 5.0/5

 
Unknown user

You are not logged in

Log in or create account
 

Link copied to your pasteboard.