iosswiftswiftuiasync-awaitmainactor

How to initialize Swift class annotated @MainActor for XCTest, SwiftUI Previews, etc


We'd like to make use of the @MainActor Annotation for our ViewModels in an existing SwiftUI project, so we can get rid of DispatchQueue.main.async and .receive(on: RunLoop.main).

@MainActor
class MyViewModel: ObservableObject {
    private var counter: Int
    init(counter: Int) {
        self.counter = counter
    }
}

This works fine when initializing the annotated class from a SwiftUI View. However, when using a SwiftUI Previews or XCTest we also need to initialize the class from outside of the @MainActor context:

class MyViewModelTests: XCTestCase {

    private var myViewModel: MyViewModel!
    
    override func setUp() {
        myViewModel = MyViewModel(counter: 0)
    }

Which obviously doesn't compile:

Main actor-isolated property 'init(counter:Int)' can not be mutated from a non-isolated context

Now, obviously we could also annotate MyViewModelTests with @MainActor as suggested here.

But we don't want all our UnitTests to run on the main thread. So what is the recommended practice in this situation?

Annotating the init function with nonisolated as also suggested in the conversation above only works, if we don't want to set the value of variables inside the initializer.


Solution

  • NOTE: For Swift 6 use override func setUp() async

    Just mark setUp() as @MainActor

    class MyViewModelTests: XCTestCase {
        private var myViewModel: MyViewModel!
    
        @MainActor override func setUp() {
            myViewModel = MyViewModel(counter: 0)
        }
    }