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

Time:11-04

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: Int)
    }

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.

CodePudding user response:

Just mark setUp() as @MainActor

class MyViewModelTests: XCTestCase {
    private var myViewModel: MyViewModel!

    @MainActor override func setUp() {
        myViewModel = MyViewModel(counter: 0)
    }
}

CodePudding user response:

Approach:

  • You can use override func setUp() async throws instead

Model:

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

Testcase:

import XCTest
@testable import Demo

final class MyViewModelTests: XCTestCase {
    private var myViewModel: MyViewModel!
    
    override func setUp() async throws {
        myViewModel = await MyViewModel(counter: 10)
    }
    
    override func tearDown() async throws {
        myViewModel = nil
    }

    func testExample() async throws {
        await myViewModel.set(counter: 20)
    }
}
  • Related