Why does a MainActor class / function not run on Main Thread?

When marking the ViewController and the function with @MainActor, the assertion to check that the UI is updated on main thread fails.

How do I guarantee that a function is run on Main Thread when using @MainActor?

Example code:

import UIKit

@MainActor
class ViewController: UIViewController {
    let updateObject = UpdateObject()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        updateObject.fetchSomeData { [weak self] _ in
            self?.updateSomeUI()
        }
    }

    @MainActor
    func updateSomeUI() {
        assert(Thread.isMainThread) // Assertion failed!
    }
}

class UpdateObject {
    func fetchSomeData(completion: @escaping (_ success: Bool) -> Void) {
        DispatchQueue.global().async {
            completion(true)
        }
    }
}

Even changing DispatchQueue.global().async to Task.detached does not work.

Tested with Xcode 13.2.1 and Xcode 13.3 RC

Replies

Change (optional)

@MainActor
func updateSomeUI() {
    assert(Thread.isMainThread) // Assertion failed!
}

to

    func updateSomeUI() {
        assert(Thread.isMainThread) // Assertion failed!
    }

Then

class UpdateObject {
    func fetchSomeData(completion: @escaping (_ success: Bool) -> Void) {
        DispatchQueue.global().async {
            completion(true)
        }
    }
}

to

class UpdateObject {
    func fetchSomeData(completion: @escaping (_ success: Bool) -> Void) {
       Task { @MainActor in 
            completion(true)
        }
    }
}

@MobileTen That doesn't address the issue. The code in question is supposed to run on the MainActor's executor. If it's not this is a bug in the runtime.

This seems to only occur when @GlobalActor isolated methods/functions are called from closures. Something to do with the runtime dispatch mechanism and this annotation-based style of isolation.

This seems to only occur when @GlobalActor isolated methods/functions are called from closures.

You mean “@MainActor isolated”, right?

As you investigate this, keep in mind that the main thread and the main queue are related but not exactly the same thing. This post has code that highlights that difference.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

  • You mean “@MainActor isolated”, right?

    No, I mean any GlobalActor isolated call relying upon @ActorName for enforcing that isolation.

Add a Comment

As you investigate this, keep in mind that the main thread and the main queue are related but not exactly the same thing. This post has code that highlights that difference.

I understand this but thousands of libraries use main thread checks as seatbelts which we can't just disable and the impression provided by what documentation exists for the MainActor is that it is a special-cased global actor with a single-threaded executor such that all calls isolated to the MainActor will be executed by the same thread. If this is not the case it breaks almost every Swift library in existence that makes display calls.

I would expect Thread.isMainThread to always return true in a block isolated to the MainActor. I actually would have used some other test of isolation but prior to 5.9 there is none and I am forced to write code that supports iOS 14.

That isn't the point of this issue though. I just used that thread test above because it is the only tool I have at my disposal to test this behaviour and MainActor is documented to behave in a manner that that test should always pass.