Understanding @MainActor in Swift: When and How to Use It



This content originally appeared on DEV Community and was authored by ArshTechPro

What are Actors in Swift?

Actors are a fundamental concurrency feature introduced in Swift 5.5 that provide a safe way to manage mutable state in concurrent programs. An actor is a reference type that protects its mutable state by ensuring that only one task can access its properties and methods at a time.

Think of an actor as a protective wrapper around data that automatically handles synchronization. When multiple tasks try to access an actor simultaneously, Swift ensures they wait in line, preventing data races and crashes that commonly occur in multi-threaded programming.

actor BankAccount {
    private var balance: Double = 0.0

    func deposit(_ amount: Double) {
        balance += amount
    }

    func getBalance() -> Double {
        return balance
    }
}

What is @MainActor?

@MainActor is a special actor that represents the main thread of an application. It’s a global actor that ensures code runs on the main dispatch queue, which is essential for UI updates and other main-thread-only operations.

The @MainActor serves as Swift’s modern replacement for manually dispatching code to the main queue using DispatchQueue.main.async. It provides compile-time safety and cleaner syntax for main-thread operations.

The key benefit of @MainActor is that it moves thread-safety concerns from runtime to compile-time. Instead of remembering to wrap UI code in dispatch calls, the compiler enforces main-thread execution automatically.

When to Use @MainActor

UI Updates and View Modifications

The most important use case for @MainActor is ensuring UI updates happen on the main thread. All UI frameworks require interface modifications to occur on the main thread to prevent crashes and ensure smooth user experience.

View Controllers and UI Classes

Apply @MainActor to entire classes that primarily deal with user interface operations, such as view controllers, UI managers, and SwiftUI view models.

Delegate Methods and UI Callbacks

When implementing delegate methods or completion handlers that update UI, @MainActor ensures they execute on the correct thread.

Animation and Visual Effects

UI animations, transitions, and visual effects must run on the main thread to work properly.

How to Use @MainActor

Class-Level Application

When applied to a class, all properties and methods become main-actor-bound, ensuring everything runs on the main thread:

@MainActor
class WeatherViewModel: ObservableObject {
    @Published var temperature: String = "--"
    @Published var condition: String = "Unknown"
    @Published var isLoading = false

    func loadWeather() async {
        isLoading = true

        // Network call happens on background thread
        let weatherData = await WeatherService.fetchCurrentWeather()

        // UI updates automatically happen on main thread
        temperature = "\(weatherData.temperature)°"
        condition = weatherData.condition
        isLoading = false
    }

    func refreshWeather() {
        Task {
            await loadWeather()
        }
    }
}

In this example, the entire WeatherViewModel class is marked with @MainActor. This means all properties (temperature, condition, isLoading) and methods (loadWeather(), refreshWeather()) automatically run on the main thread. When loadWeather() updates the @Published properties, SwiftUI will receive these changes on the main thread, ensuring smooth UI updates.

Method-Level Application

For classes that mix UI and background work, specific methods can be marked to run on the main actor:

class DataManager {
    private var cache: [String: Any] = [:]

    // Background work - not on main thread
    func fetchUserData(userId: String) async -> UserData {
        let userData = await NetworkService.getUserData(userId: userId)
        cache[userId] = userData
        return userData
    }

    // UI update - must be on main thread
    @MainActor
    func updateUserInterface(with userData: UserData) {
        NotificationCenter.default.post(
            name: .userDataUpdated,
            object: userData
        )
    }
}

Property-Level Application

Individual properties can be marked with @MainActor for more granular control:

class AppSettings {
    @MainActor var currentTheme: Theme = .light

    var apiTimeout: TimeInterval = 30.0  // Background property
    var retryCount: Int = 3              // Background property

    @MainActor
    func updateTheme(_ newTheme: Theme) {
        currentTheme = newTheme
        // UI notification runs on main thread
        NotificationCenter.default.post(name: .themeChanged, object: currentTheme)
    }

    func updateThemeFromBackground(_ newTheme: Theme) async {
        // Must use await when calling @MainActor method from background
        await updateTheme(newTheme)
    }
}

In this example, only currentTheme is marked with @MainActor, ensuring theme access happens on the main thread, while other properties remain accessible from any thread.

Key Benefits of @MainActor

Compile-Time Safety

@MainActor provides compile-time guarantees that UI code runs on the main thread, preventing runtime crashes that occur when UI is accessed from background threads.

Cleaner Code

Eliminates the need for manual DispatchQueue.main.async calls, making code more readable and less error-prone.

Seamless Integration with Async/Await

Works naturally with Swift’s modern concurrency features, allowing smooth transitions between background and main-thread work.

SwiftUI Compatibility

Essential for SwiftUI applications where view updates must happen on the main thread to trigger proper UI refreshes.

Best Practices

Apply at the Right Level

  • Use method-level @MainActor for specific UI update methods in mixed-purpose classes

Keep Main Thread Work Light

Reserve @MainActor code for UI updates and light operations. Perform heavy computations on background threads before updating the UI.

Understand Async Context

Remember that calling @MainActor code from background contexts requires await and creates a suspension point where the function may be paused while switching to the main thread.


This content originally appeared on DEV Community and was authored by ArshTechPro