< How to use @MainActor to run code on the main queue | What is actor hopping and how can it cause problems? > |
Updated for Xcode 14.2
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.
SAVE 50% To celebrate WWDC23, all our books and bundles are half price, so you can take your Swift knowledge further without spending big! Get the Swift Power Pack to build your iOS career faster, get the Swift Platform Pack to builds apps for macOS, watchOS, and beyond, or get the Swift Plus Pack to learn advanced design patterns, testing skills, and more.
Link copied to your pasteboard.