Do I need to explicitly annotate UIViewController subclasses' instance methods with @MainActor?

Hi all, I'm trying to adopt async-await into my existing UIKit codebase (I finally could just elevate the deployment target to iOS 13 thanks to Xcode 16, so there is barely any SwiftUI code in my codebase), but unfortunately there's hardly any docs designated for UIKit adjustments. I'm new to Swift Concurrency, so bear with me if the answer is too obvious.

I know that UIViewController and all its subclasses are implicitly @MainActor, so all instance methods are @MainActor, too. When I option-click on the function's name, Xcode tells me that the method is indeed @MainActor.

So, is it safe to say that the runtime will make sure the content of the method is always scheduled on the main thread, no matter what context the caller site lies in?

For example, this code below describes a simple UIViewController that fetches the data model from the server in its viewDidLoad method:

class CustomViewController: UIViewController {

    private var models: [DataModel] = []
    
    // some UI elements...

    override func viewDidLoad() {
        super.viewDidLoad()
        
        // some UI configurations

        Task {
            await loadModels()
        }
    }

    private func loadModels() async {
        view.showLoadingView()
        do {
            let models = try await DataService.models()
            setupUI(with: models)
            view.hideLoadingView()
        } catch {
            view.showErrorView(error: error, retryButtonAction: { [self] in
                Task {
                    await loadModels()
                }
            })
        }
    }

    private func setupUI(with models: [DataModel]) {
        self.models = models // self related
        setupContentView() // UI related
    }

}

I know the loadModels and setupUI are implicitly @MainActor, so I didn't explicitly annotates them. However, since I called loadModels in a Task {} , can I rest assured that view.showLoadingView() is on the main thread?

What about the setupUI call after the await? Does it switch over to the main thread automatically? Is the view.hideLoadingView() safe?

Moreover, I read somewhere in this forum that I can instruct the task to run on the main actor by:

Task { @MainActor in
    await loadModels()
 }

Does this change impact the answer to the above questions? Or, do I need to explicitly annotate loadModels() with @MainActor?

Finally, in the retryButtonAction block, I have to explicitly capture self because it is an escaping closure. But in the Task {}, I don't need to do it again. What thread will the line await loadModels() run on?

1 Like

Hi HoustonDuane.

Most of your questions can be answered by the Swift 6 Migration Guide, specifically this section: Documentation

But overall yes, in your example everything is guaranteed to run on the Main Actor as everything is defined within the scope of a Main Actor isolation domain, and you're not using Task.detached.

2 Likes

A million thanks to linking me to that documentation!

1 Like