Nirvan Nagar
Nirvan Nagar

Reputation: 369

Why Should SwiftUI View Models be Annotated with @MainActor?

I've been watching Apple's concurrency talks from WWDC21 as well as reading a ton of articles on Apple's concurrency updates; however, I cannot wrap my head around one thing: Why do people provide guidance that you should annotate view models with @MainActor? From what I have read, by making a view model property a @StateObject or @ObservedObject within a view, it automatically becomes @MainActor. So if that's the case, why do people still recommend annotating view models as @MainActor?

For context, here are a few of the articles I've read that reference this:

  1. https://www.hackingwithswift.com/quick-start/concurrency/understanding-how-global-actor-inference-works
  2. https://www.hackingwithswift.com/books/concurrency/how-to-use-mainactor-to-run-code-on-the-main-queue
  3. https://peterfriese.dev/swiftui-concurrency-essentials-part1/

Excerpt from the first link:

[A]ny 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.

Excerpt from the second link:

[W]henever you use @StateObject or @ObservedObject inside a view, Swift will ensure that the whole view runs on the main actor so that you can’t accidentally try to publish UI updates in a dangerous way. Even better, no matter what property wrappers you use, the body property of your SwiftUI views is always run on the main actor.

Does that mean you don’t need to explicitly add @MainActor to observable objects? Well, no – there are still benefits to using @MainActor with these classes, not least if they are using async/await to do their own asynchronous work such as downloading data from a server.

All in all, I'm a bit confused by the guidance if it would be handled for us automatically. Especially since I don't know of a scenario where you'd have a view model in SwiftUI that is not an @ObservableObject.

My last question, related to the first question, is: If @StateObject and @ObservedObject automatically make the view @MainActor, then does @EnvironmentObject also make a view @MainActor?

To put some code behind this, I intend to have the following class injected into the environment using .environmentObject(...)

@MainActor
class UserSettings: ObservableObject {
    @Published var flowUser: FlowUser?
    
    init(flowUser: FlowUser? = nil) {
        self.flowUser = flowUser
    }
}

And the following is a view model for one of my views:

@MainActor
class CatalogViewModel: ObservableObject {
    @Published var flowUser: FlowUser?
    
    init(flowUser: FlowUser?) {
        self.flowUser = flowUser
    }
}

As you can see, I have made both classes @ObservableObjects so I feel like I should be able to remove the @MainActor annotation.

Any help would be greatly appreciated! Thanks for your time!

Upvotes: 21

Views: 11020

Answers (3)

yo1995
yo1995

Reputation: 637

Here is the question I posted on Swift Forum: https://forums.swift.org/t/using-mainactor-on-an-observableobject-or-a-method/65629

I have been thinking about this question for quite a while and still cannot come up with a definitive answer. Most Apple resources I could have found implicate that MainActor should be used with ObservableObject in most common cases when you are doing a Model-View pattern development.

A few other takeaways…

  • Apple doesn't like the type ViewModel or the phrase "view model" in their SwiftUI docs. I have so far spotted only 1 occurrence of "view model" in the docs. All other places refer to the model object as a "data model"
  • If a data model is serving a view, it is likely that at some point you need to add MainActor to almost all methods, due to the so called "async contamination/infection/cascading" problem. In that sense, marking the whole object as MainActor is kind of an interim solution.
  • …which brings us to iOS 17.0+, where Observable macro replaces ObservableObject. I guess Apple may gradually phase out ObservableObject in the future, just like Combine.

Upvotes: 1

Cecilia_Chen
Cecilia_Chen

Reputation: 21

My understanding is that

  1. if your ViewModel might be used in contexts other than SwiftUI (for example, in a UIKit-based part of your app or in a more general Swift context), explicitly marking it with @MainActor can ensure that it will behave correctly. We all know SwiftUI views are inherently main-thread-bound, and their lifecycle and state changes are managed on the main thread, but it might not be the case if your viewModel be used in other places.
  2. explicitly annotating a ViewModel with @MainActor can serve as an indication to anyone reading the code that this is intended to be used on the main thread. It can be helpful in larger teams in order to improve readability.

Upvotes: 1

doozMen
doozMen

Reputation: 730

An @ObservedObject or the others does not make it a main actor. So your statement is not true

see https://developer.apple.com/videos/play/wwdc2021/10019/ around minute 22 enter image description here

From what I have read, by making a view model property a @StateObject or @ObservedObject within a view, it automatically becomes @MainActor.

Upvotes: 9

Related Questions