tldr – marking methods with @MainActor does not guarantee that they run on the main thread.
Swift has a magical new structured concurrency model.
‘using Swift’s language-level support for concurrency in code that needs to be concurrent means Swift can help you catch problems at compile time’
-the swift programming language
One of the new features is @MainActor. You can use this to annotate functions, classes, properties, etc.
link
this proposal introducesMainActor
as a global actor describing the main thread. It can be used to require that certain functions only execute on the main thread
So – what does @MainActor do?
I hoped to find some documentation, but other than the evolution proposal, I haven’t found any.
There are some tutorials out there which say things like:
https://www.swiftbysundell.com/articles/the-main-actor-attribute/
all that we really need to know is that this new, built-inactor
implementation ensures that all work that’s being run on it is always performed on the main queue.
This is what I thought @MainActor did. It is not true.
I created an example to demonstrate.
I have a ViewController with a method and property marked with @MainActor
@MainActor var mainDate:Date { print("@MainActor var - main: \(Thread.isMainThread)") return Date() } @MainActor func printDate(_ label:String) { print("@MainActor func - \(label) - main: \(Thread.isMainThread)") }
My expectation is that calling the function printDate or the property mainDate will always run the code on the main thread. As such, they’ll always print “main: true” in their debug statements.
I created an async function which calls these
func doWork() async { let _ = await Background().go() print("returned from Background - now running off main thread") print("calling mainDate in doWork") self.storedDate = self.mainDate //sometimes not main thread printDate("in doWork") //sometimes not main thread }
The first call in the async function is to a background actor.
//Actor that isn't main - it does not run on the main thread actor Background { func go() -> Date { print("Background go - main: \(Thread.isMainThread)") return Date() } }
The effect of the background actor is simply to return from the await on a background (not main) thread.
This ensures that I’m not simply calling mainDate and printDate on the main thread by default.
I call doWork from a task responding to a button click
@IBAction func doWorkInAsyncFunction(_ sender: Any) { Task { @MainActor in await doWork() } }
For good measure – I mark the task as @MainActor.
My expectation was the following
- in doWork(), it would return off the main thread after Background().go()
- @MainActor annotation would ensure that calling self.mainDate would happen on the main thread – or there would be a compiler error
- @MainActor annotation would ensure that calling self.storedDate would happen on the main thread – or there would be a compiler error
#2 and #3 are false. I get the following output when I run the code
returned from Background - now running off main thread calling mainDate in doWork @MainActor var - main: false @MainActor func - in doWork - main: false
Note the last two lines print main:false – @MainActor isn’t keeping me on the main thread here…
Bizarrely – If include a print statement in my task, then everything _does_ run on the main thread!!!
@IBAction func doWorkInAsyncFunctionWithPrint(_ sender: Any) { Task { @MainActor in //Adding this print statement, magically makes everything in doWork run on the main thread!!! print("Srsly?") await doWork() } }
gives the following “correct” output
Srsly? Background go - main: false returned from Background - now running off main thread calling mainDate in doWork @MainActor var - main: true @MainActor func - in doWork - main: true
Similarly – if I call identical functions, but within a Task (and not even one annotated as @MainActor), then I get the “correct” results
@IBAction func doInTask(_ sender: Any) { Task { let _ = await Background().go() print("returned from Background - now running off main thread") print("calling mainDate doInTask") self.storedDate = self.mainDate //main thread printDate("in doInTask") // main thread } }
gives the following…
Background go - main: false returned from Background - now running off main thread calling mainDate doInTask @MainActor var - main: true @MainActor func - in doInTask - main: true
Is this even wrong?
Swift isn’t behaving as I would expect it to. That’s hardly damning!
However:
- I can’t find any formal documentation to compare against.
- I can’t look up what the guarantee (if any) is that Swift provides when you annotate something with @MainActor
I have mostly learned from the brilliant resources provided by sites like swiftbysundell
all that we really need to know is that this new, built-in
-swiftbysundellactor
implementation ensures that all work that’s being run on it is always performed on the main queue.
The magic of
-hacking with swift@MainActor
is that it automatically forces methods or whole types to run on the main actor, a lot of the time without any further work from us.
It seems that I’m not alone in what I expected @MainActor to do. Perhaps this is a bug. Perhaps it is expected behaviour. In the absence of clear documentation showing what @MainActor should to, I can’t tell.
Either way – marking methods with @MainActor does not ensure that they run on the main thread.
Update: Warnings Available…
If you’re writing Swift concurrency code, add these compiler flags:
-Xfrontend -warn-concurrency -Xfrontend -enable-actor-data-race-checks
(in Xcode: Other Swift Flags)
Warnings in Swift 5.5 identify unsafe constructs, will become errors in Swift 6. https://t.co/kWEGwlH0rR
— Ole Begemann (@olebegemann) July 30, 2021
returned from Background - now running off main thread calling mainDate in doWork warning: data race detected: @MainActor function at MainActorExample/ViewController.swift:26 was not called on the main thread 2022-01-17 14:43:40.786070+0000 MainActorExample[4407:7439502] warning: data race detected: @MainActor function at MainActorExample/ViewController.swift:26 was not called on the main thread @MainActor var - main: false warning: data race detected: @MainActor function at MainActorExample/ViewController.swift:31 was not called on the main thread 2022-01-17 14:43:40.811866+0000 MainActorExample[4407:7439502] warning: data race detected: @MainActor function at MainActorExample/ViewController.swift:31 was not called on the main thread @MainActor func - in doWork - main: false
This does show runtime a bunch of warnings.
‘cannot use parameter ‘self’ with a non-sendable type ‘ViewController’ from concurrently-executed code’
and
‘cannot call function returning non-sendable type ‘Date’ across actors’
I’ll experiment some more…
Update 2: Models get errors!
Moving the exact same code out of the NSViewController and into a separate class (that inherits from nothing) causes the expected compiler warnings to kick in.

This feels like an important limitation that should be in BIG BOLD LETTERS in the documentation…
Interestingly, when this code is moved to a separate model, calling self.mainDate without async causes an error.
However in the original ViewController version – adding an async (async self.mainDate) generates a warning
‘no ‘async’ operations occur within ‘await’ expression’
Update 3
This bug looks like the same setup
Update 4 – May 2022
Running the example code in Xcode 13.3.1 on OSX 12.3.1 and the code behaves as expected.
I don’t know if this is a compiler fix (so newly compiled apps are fine), or an OS fix (so your app will only behave unexpectedly on older OS’s.)
Example code available at https://github.com/ConfusedVorlon/MainActorExample