In short, something
has to be modified from the main thread and only Sendable types can be passed from one actor to another. Let’s dig in the details.
something
has to be modified from the main thread. This is because @Published
properties in an ObservableObject
have to be modified from the main thread. The documentation for this is lacking (if anyone finds a link to the official documentation I’ll update this answer). But as the subscriber of an ObservableObject
is probably a SwiftUI View
, it makes sense. Apple could have decided that a View
subscribes and receives events on the main thread, but this would hide the fact that it is dangerous to send UI update events from multiple threads.
Only Sendable types can be passed from one actor to another. There are two ways to solve this. First we can make a
Sendable. Second we can make sure not to pass a
across actor boundaries and have all code run on the same actor (in this case it has to be the Main Actor as it is guaranteed to run on the main thread).
Let’s see how to make a
sendable and study the case of:
await MainActor.run {
something = a
}
The code in doVariousStuff()
function can run from any actor; let’s call it Actor A. a
belongs to Actor A and it has to be sent to the Main Actor. As a
does not conform to Sendable, the compiler does not see any guarantee that a
will not be changed while a
is read on the Main Actor. This is not allowed in the Swift concurrency model. To give the compiler that guarantee, a
has to be Sendable. One way to do that is to make it constant. Which is why this works:
let c = a
await MainActor.run {
something = c
}
Even if it could be improved to:
await MainActor.run { [a] in
something = a
}
Which captures a
as a constant. There are other Sendable types, details can be found here https://docs.swift.org/swift-book/LanguageGuide/Concurrency.html#ID649.
The other way to solve this is to make all code run on the same actor. The easiest way to do that is to mark ViewModel
with @MainActor
as suggested by Asperi. This will guarantee that doVariousStuff()
runs from the Main Actor, so it can set something
. As a side note, a
then belongs to the Main Actor so (even if it is pointless) await MainActor.run { something = a }
would work.
Note that actors are not threads. Actor A can run from any thread. It can start on one thread and then continue on another after any await
. It could even run partially on the main thread. What is important is that one actor can only ever run from one thread at a time. The only exception to the rule that any actor can run from any thread is for the Main Actor which only runs on the main thread.