skip to Main Content

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.

2

Answers


  1. Just mark setUp() as @MainActor

    class MyViewModelTests: XCTestCase {
        private var myViewModel: MyViewModel!
    
        @MainActor override func setUp() {
            myViewModel = MyViewModel(counter: 0)
        }
    }
    
    Login or Signup to reply.
  2. 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)
        }
    }
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search