Why Should SwiftUI View Models be Annotated with @MainActor?
Asked Answered
M

3

17

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!

Milkwort answered 14/12, 2021 at 19:52 Comment(8)
They (published) update UI (in majority of cases) and UI must(!) be updated only on main queue.Aristarchus
If you want to know whether you can remove the @MainActor attribute, why don't you try removing it and see what happens?Landtag
We don't use view model objects in SwiftUI (that is what the View struct is for) and to make one via@StateObject would be misusing what it is designed for.Whisker
@Whisker "We don't use view model objects in SwiftUI" not true, if you don't use ViewModels doesn't mean no one is using them. Most of the SwiftUI apps I've seen are MVVM...Dement
@Dement you’ll see less and less as they take time to learn the View structWhisker
@Whisker View models are still widely used in my experience too. The Observable addition in 2023 made them easier than ever, and it's a great way to separate out logic from the view. Apple speakers still explicitly mention them, and use them in several other WWDCs where they aren't mentioned by name. Google query to see them referenced explicitly: site:developer.apple.com/videos/play "view model"Barmaid
@LouZell I think those that use view models didn't have time to learn the View struct and would rather use familiar classes. Unfortunately Objects are a bad idea to separate logic since there is the overhead of a heap object and the risk of consistency bugs from reference semantics - Swift and SwiftUI are designed to use value semantics for robustness. Apple use structs to separate logic but the MVVM crowd can't because don't know about mutating func. Apple used to use objects for async logic but now use .task which gives the correct lifetime reference semantics so no need for an object.Whisker
Observable was introduced in 2023 and only works with reference types. It is quite a leap to suggest that no one responsible for it understands the basics of mutating structs. The first minute of the session states: "You can use Observable types to power your SwiftUI views", which sounds like a view model to me. Part of the problem is no one agrees on a VM's responsibility... for me it is preparing data for use so that the view can remain view-only. In Ben Cohen's concurrency code-along, he calls them UI models. That is from someone that without a shred of doubt understands swift's intricaciesBarmaid
I
8

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.

Iand answered 28/1, 2022 at 9:40 Comment(1)
In fact, property wrappers can influence actor isolation today. But, that is changing, thankfully, because it has caused so much confusion. github.com/apple/swift-evolution/blob/main/proposals/…Protectorate
D
1

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.
Drudge answered 16/12, 2023 at 1:11 Comment(1)
I'm not that sure, that SwiftUI views have to be main thread confined. Hypothetically, the idea could be as well, that the view initialiser can be called on any thread if the underlying system which stores the values is thread-safe. The system then can read the values and perform the rendering on any other thread. This would require only, that the values passed must be @Sendable.Our
G
1

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.
Graduation answered 11/6, 2024 at 23:21 Comment(1)
Does usage of @Observable guarantees the @MainActor behaviour as well?Celestyna

© 2022 - 2025 — McMap. All rights reserved.