@MainActor isn't forcing compiler check when mutating @Observable property on background thread

157 Views Asked by At

Why does the below code not create a compiler error?

I am mutating a @Published property of this ObservableObject on a background thread. Shouldn't the @MainActor tag on this class mean that any code which mutates a published property must occur on the main thread?

import SwiftUI

@MainActor
class ViewModel: ObservableObject {
        
    @Published private(set) var questionnaire: Questionnaire!
    
    init() {
        fetchQuestionnaire(id: "forest")
    }
    
    func fetchQuestionnaire(id: String) {
        Task {
            questionnaire = Questionnaire.testData
        }
    }
    
}

Or, is it actually OK to mutate @Published properties on a background thread, and SwiftUI will perform the didSet publisher on the main thread?

And if that’s the case, what even really is the point of @MainActor in this situation?

1

There are 1 best solutions below

0
On

When you isolate ViewModel to the main actor, its methods and properties are isolated to the main actor, too. In fact, this practice of isolating the ObservableObject to the main actor is actually recommended in WWDC 2021’s video Discover concurrency in SwiftUI. In that video, they recommend isolating the ObservableObject being observed by a View to the main actor.

And when you use Task {…} within an actor isolated method, that creates a new top level task on the current actor (i.e., the main actor). As Vadian pointed out, the documentation says (emphasis added):

[Task {…} runs] the given nonthrowing operation asynchronously as part of a new top-level task on behalf of the current actor.

So, bottom line, you are not updating this on a “background thread”. You are updating it on the main actor. Hence, no compiler errors and no runtime warnings from the main thread checker.