Observation and MainActor

Previously, it was recommended to use the @MainActor annotation for ObservableObject implementation.

@MainActor
final class MyModel: ObservableObject {
    let session: URLSession

   @Published var someText = ""

  init(session: URLSession) {
   self.session = session
  }
}

We could use this as either a @StateObject or @ObservedObject:

struct MyView: View {
  @StateObject let model = MyModel(session: .shared)
}

By moving to Observation, I need to the @Observable macro, remove the @Published property wrappers and Switch @StateObject to @State:

@MainActor
@Observable
final class MyModel {
    let session: URLSession

   var someText = ""

  init(session: URLSession) {
   self.session = session
  }
}

But switching from @StateObject to @State triggers me an error due to a call to main-actor isolated initialiser in a synchronous nonisolated context.

This was not the case with @StateObject of @ObservedObject.

To suppress the warning I could :

  • mark the initializer as nonisolated but it is not actually what I want
  • Mark the View with @MainActor but this sounds odd

Both solutions does not sound nice to my eye.

Did I miss something here?

Post not yet marked as solved Up vote post of yageekCH Down vote post of yageekCH
3.3k views

Replies

Your use of URLSession and @StateObject likely means you are attempting to do async networking in this object so it shouldn't be @MainActor because you'll want your async funcs to run on background threads not on the main thread.

Also, when switching from @ObservedObject to @Observable you no longer need to use Task { @MainActor in when setting vars (@Published in case of @ObservedObject) with the results of the network calls.

  • Where is this network call related evolution mentioned 😅 ?

  • Regarding your first paragraph, no, OP does want @MainActor here. @MainActor means that all updates to the object will occur in the main thread. Any networking called within the @MainActor object can still take place in different threads, and the downloads themselves can take place somewhere else. Using @MainActor means their content will be delivered to the main thread, which is what you want with code that updates the UI. It does not mean async calls will take place in the main actor.

  • For reference on, see https://docs.swift.org/swift-book/documentation/the-swift-programming-language/concurrency/

    It boils down to every 'await' call invoking a new opportunity to switch threads.

I wonder as well. I have this code that I'm not sure what will be the behavior now:

import Foundation import Combine import SwiftUI


@Observable
open class BaseViewmodel {
    
    var cancellation = [AnyCancellable]()
    
    public init() {
        print("Init: \(self)")
    }
    
    deinit {
        print("Deinit: \(self)")
    }
}

and:

@Observable
@MainActor
class HomeViewModel: BaseViewmodel {
    
    override init() {
        super.init()
    }
}

if I'll add @MainActor to the base class I will get same error:

Call to main actor-isolated initializer 'init()' in a synchronous nonisolated context

Now that @Observable macro has been out a little bit, what's the latest thinking on this? 🤔

How would one apply @MainActor to the ViewModel of the following code? Currently, doing so gives the "Call to main actor-isolated initializer 'init()' in a synchronous nonisolated context" error on the @State line in the TestApp struct.

import SwiftUI

@main
struct TestApp: App {
    @State private var vm = ViewModel()

    var body: some Scene {
        WindowGroup {
            ContentView()
                .environment(vm)
        }
    }
}

@Observable 
@MainActor
class ViewModel {
   var showDetails: Bool = false
}

struct ContentView: View {
   @Environment(ViewModel.self) var vm

   var body: some View {
       @Bindable var vm = vm
       VStack {
           DetailsButton(showDetails: $vm.showDetails)
           if vm.showDetails {
               Text("This is my message!")
           }
       }
   }
}

struct DetailsButton: View {
   @Binding var showDetails: Bool

   var body: some View {
       Button("\(showDetails ? "Hide" : "Show") Details") {
           showDetails.toggle()
       }
   }
}
  • You shouldn't do @State private var vm = ViewModel() because your ViewModel is a class. You would be better using the View as the view model and declaring @State for your data, e.g. @State var showDetails: Bool = false. You could group related state vars into a struct if you like but not a class. You can use mutating func for logic related to the vars.

Add a Comment

I have continued to apply @MainActor to my VMs. In the old world, having a @StateObject used to infer a @MainActor for the whole View, see here. The @State doesn't make the same inference, which is the topic of that SE thread. Since the new @Observable macro doesn't make any assumptions about the actor that the property is observed on, I think we should continue to explicitly mark VMs as @MainActor, and manually mark the view as @MainActor, since this was already happening under the hood.

This is what I ended up doing. I applied the @MainActor macro to my MainView as I'm using Firebase, and I have to keep that on the main thread for detecting the rootViewController from their SDK.

@thecannabisapp @darvishk so is this the only possible solution to this case ? coz I'm having the same scenario: Observation & @MainActor class

  • Would be nice if Apple will update "Quake" same, too. So we can have:

    full working sample for iOS 17"blessed" code
Add a Comment

Bumping this, also interested

Also interested on what the official recommendation is here. Encountering many crashes since migrating to @observable