← All shorts
57Swift 5.5+2 min read

@MainActor: What It Actually Guarantees

@MainActor guarantees isolation to the main actor's serial executor, which in practice means the main thread, not synchronous execution.

Every background callback that touched UI needed a DispatchQueue.main.async hop, placed at exactly the right point. Miss one and you get a UIKit assertion, or worse, a silently corrupted layout pass.

#The old way

final class FeedViewModel {
    var posts: [String] = []
    var onUpdate: (() -> Void)?

    func refresh() {
        NetworkClient.fetchPosts { [weak self] result in
            guard let self = self else { return }
            DispatchQueue.main.async {
                self.posts = result
                self.onUpdate?()
            }
        }
    }
}

#The new way

import Observation

@MainActor
@Observable
final class FeedViewModel {
    var posts: [String] = []

    func refresh() async {
        let result = await NetworkClient.fetchPosts()
        posts = result
    }
}

enum NetworkClient {
    static func fetchPosts() async -> [String] {
        ["First post", "Second post"]
    }
}

#Why it matters

  • Every property and method on FeedViewModel is guaranteed to run on the main actor — no manual dispatch needed anywhere in the class
  • SwiftUI views can read and mutate the view model directly without wrapping every call site in a queue hop
  • nonisolated methods and background async functions can still be called from inside a @MainActor type via await, so networking code doesn't need to live on the main actor too
  • Isolation is checked at compile time, not discovered at runtime when an assertion fires

#Gotcha

@MainActor promises which executor runs the work. It promises nothing about when.

Call a @MainActor async function from non-main-actor code and you still suspend at the await, and the call gets scheduled onto the main actor's queue. The calling thread does not block until the main actor is free. And if other main-actor work is already queued ahead of yours, the code after the await is not guaranteed to run before the next screen render.

swiftconcurrency

Related shorts