cd /news/developer-tools/mainactor-in-swift-explained-with-co… · home topics developer-tools article
[ARTICLE · art-83695] src=avanderlee.com ↗ pub= topic=developer-tools verified=true sentiment=· neutral

@MainActor in Swift explained with code examples

Apple's Swift 6.2 introduces Default Actor Isolation in new Xcode 26 projects, automatically isolating app target code to the main actor, reducing the need for explicit @MainActor annotations. The @MainActor attribute, a global actor from the standard library, still functions identically, ensuring main-thread execution for properties, methods, instances, and closures, as explained by Antoine van der Lee in a SwiftLee article.

read10 min views1 publishedAug 2, 2026
@MainActor in Swift explained with code examples
Image: Avanderlee (auto-discovered)

@MainActor is a global actor that performs its tasks on the main thread. You can use it to dispatch to the main thread by marking properties, methods, instances, or closures with the attribute. Instead of manually dispatching using DispatchQueue.main.async

, you let the compiler enforce main thread execution for you.

If you’re new to Actors in Swift, I recommend reading my article Actors in Swift: how to use and prevent data races first. The main actor behaves like other actors, with one important difference: it performs all its tasks on the main thread.

Let AI Control the iOS SimulatorRocketSim lets AI coding agents inspect, navigate, and verify your running iOS Simulator app through a CLI and Agent Skill for Cursor, Claude, Codex, and Xcode. Give agents real UI feedback instead of letting them guess from code alone.

See how it works.

What is a MainActor?

A MainActor is a globally unique actor that performs its tasks on the main thread. You can use it for properties, methods, instances, and closures to perform tasks on the main thread. Proposal SE-0316 Global Actors introduced the main actor as an example of a global actor that inherits the GlobalActor

protocol.

Do you still need @MainActor in Swift 6.2 and later?

Since Swift 6.2, your code might already run on the main actor without a single annotation. New projects in Xcode 26 and later enable Default Actor Isolation, which isolates all code inside your app target to the main actor unless you opt out. In other words: if you’re working inside a new project, you often don’t need to add @MainActor yourself anymore.

This change is part of a bigger set of improvements that I’ve covered in Approachable Concurrency in Swift 6.2: A Clear Guide. For this article, it’s important to know that the attribute still works exactly the same. Whether you write @MainActor yourself or the compiler infers it, the same rules apply.

MainActor: a Global Actor from the standard library

The MainActor is a so-called global actor: an actor with a globally unique, shared instance. Its implementation is available by default and looks as follows:

@globalActor
final actor MainActor: GlobalActor {
    static let shared: MainActor
}

Anywhere you use the @MainActor attribute, you’ll synchronize through this shared actor instance, ensuring mutually exclusive access to declarations. You can also define custom global actors, as I’ve explained in Global actor in Swift Concurrency explained with code examples.

How to use MainActor in Swift?

You can use a global actor with properties, methods, closures, and instances. For example, we could add the @MainActor

attribute to a view model to perform all tasks on the main thread:

@MainActor
final class HomeViewModel {
    // ..
}

Using nonisolated, we ensure methods without the main thread requirement perform as fast as possible by not waiting for the main thread to become available. You can only annotate a class with a global actor if it has no superclass, the superclass is annotated with the same global actor, or the superclass is NSObject. A subclass of a global-actor-annotated class must be isolated to the same global actor.

In other cases, we might want to define individual properties with a global actor:

final class HomeViewModel {
    
    @MainActor var images: [UIImage] = []

}

Marking the images

property with the @MainActor

property ensures that it can only be updated from the main thread:

This is great when working with MVVM in SwiftUI as you only want to trigger view redraws on the main thread.

You can mark individual methods with the attribute as well:

@MainActor func updateViews() {
    /// Will always dispatch to the main thread as long as it's called
    /// within a concurrency context.
}

Be warned: this method is only guaranteed to run on the main thread when you call it from a concurrency context. The Swift 6 language mode catches most of these cases at compile time. I’ll dive deeper into this pitfall later in this article.

And finally, you can mark closures to perform on the main thread:

func updateData(completion: @MainActor @escaping () -> ()) {
    Task {
        await someHeavyBackgroundOperation()
        await completion()
    }
}

Although in this case, you should rewrite the updateData

method to an async variant without needing a completion closure.

FREE 5-Day Email Course: The Swift Concurrency Playbook

A FREE 5-day email course revealing the 5 biggest mistakes iOS developers make with with async/await that lead to App Store rejections And migration projects taking months instead of days (even if you've been writing Swift for years)

Using the main actor directly

The MainActor in Swift comes with an extension to use the actor directly:

extension MainActor {

    /// Execute the given body closure on the main actor.
    public static func run<T>(resultType: T.Type = T.self, body: @MainActor @Sendable () throws -> T) async rethrows -> T
}

This lets us call @MainActor in methods without using its attribute in the body.

Task {
    await someHeavyBackgroundOperation()
    await MainActor.run {
        // Perform UI updates
    }
}

In other words, there’s no need to use DispatchQueue.main.async

anymore. However, I do recommend using the global attribute to restrict any access to the main thread. Without the global actor attribute, anyone could forget to use MainActor.run

, potentially leading to UI updates taking place on a background thread.

MainActor.run vs. Task { @MainActor in }

Another common way of dispatching to the main thread is by isolating the closure of a task:

Task { @MainActor in
    imageView.image = image
}

The difference with MainActor.run

is subtle but essential. A task with a main actor isolated closure performs all of its work on the main actor, while MainActor.run

only switches over for the given closure:

Task {
    /// Runs somewhere in the background
    let image = try await downloadImage()

    /// Explicitly switch over to the main actor
    await MainActor.run {
        imageView.image = image
    }
}

I prefer to use MainActor.run

when only part of my task requires execution on the main thread. If the whole task is about updating the UI, I isolate the closure using Task { @MainActor in }

instead.

Verifying main actor execution with assumeIsolated

In rare cases, you might know for sure that your code runs on the main thread while the compiler can’t verify it. You can use MainActor.assumeIsolated

to access main actor isolated code from a synchronous, nonisolated context:

nonisolated func handleCallback() {
    MainActor.assumeIsolated {
        updateViews()
    }
}

Be careful: assumeIsolated

does not dispatch to the main thread for you. Instead, it stops program execution when it’s called from any other thread. In other words, only use it when you’re 100% sure your code already runs on the main thread.

When should I add the @MainActor attribute?

Whether you should add the attribute yourself depends on your project configuration. If your project uses Swift 6.2’s default actor isolation, most of your code is already isolated to the main actor. The question becomes the opposite: when should you opt out? You can use nonisolated or @concurrent for any code that shouldn’t block the main thread, like heavy processing tasks.

In projects without default isolation, I recommend adding @MainActor to any type or method that updates UI. Before Swift Concurrency, you might have written several dispatch statements to ensure callbacks arrive on the main thread. By rewriting your code to use async/await and the main actor, you allow dispatching to only take place if needed:

@MainActor
func fetchImage(for url: URL) async throws -> UIImage {
    let (data, _) = try await URLSession.shared.data(from: url)
    guard let image = UIImage(data: data) else {
        throw ImageFetchingError.imageDecodingFailed
    }
    return image
}

The @MainActor attribute ensures the logic executes on the main thread while the network request is still performed on a background thread. Dispatching to the main actor occurs only when needed, ensuring the best possible performance.

Can you annotate an actor with @MainActor?

No, and the compiler will tell you an actor cannot have a global actor attribute. An actor defines its own isolation, which conflicts with isolating it to the global main actor. If you need a type that performs all of its work on the main thread, you should use a @MainActor annotated class instead.

Understanding Why @MainActor Doesn’t Always Ensure Main Thread Execution

If you’re new to concurrency, you might think marking a method with @MainActor always ensures it runs on the main thread. Unfortunately, this is not guaranteed for synchronous methods in non-isolated contexts. Several teams found out the hard way, just like Cyril Cermak, who reached out to me during one of my conference visits.

Before sharing the details, it’s good to know that Swift 6 language mode catches most of these occasions. However, since many teams are still migrating to Swift 6, I wanted to highlight this misunderstanding.

Synchronous methods in non-isolated contexts run on the same thread as the caller, regardless of any actor annotations. Depending on which Swift language mode you’re using, you might or might not be warned about potential issues.

For example, this code compiles fine in Swift 5 language mode:

The dispatch via the global queue creates a non-isolated context and the @MainActor attribute isn’t respected.

However, plenty of projects are still built with Swift 5, so it’s an essential pitfall to be aware of.

In larger projects, it’s likely that a chain of methods is called on a background thread. Eventually, such a chain can end up calling a method attributed to the main actor while synchronously dispatched to the same thread as the caller: a background thread. Therefore, see if you can get your code on Swift 6; otherwise, be sharp when using the main actor attribute.

You might not directly encounter issues if you always call the synchronous method from a source running on the main thread. However, awareness of this subtle difference is essential since your expectations might not align, resulting in unexpected UI-related crashes before migrating to Swift 6.

Continuing your journey into Swift Concurrency

The concurrency changes are more than just actors and include other features that you can benefit from in your code. So, while at it, why not dive into other concurrency features?

Actors in Swift: how to use and prevent data racesGlobal actor in Swift Concurrency explained with code examplesNonisolated and isolated keywords: Understanding Actor isolationDefault Actor Isolation in Swift 6.2Threads vs. Tasks in Swift ConcurrencySwift 6: What’s New and How to Migrate

Let AI Control the iOS SimulatorRocketSim lets AI coding agents inspect, navigate, and verify your running iOS Simulator app through a CLI and Agent Skill for Cursor, Claude, Codex, and Xcode. Give agents real UI feedback instead of letting them guess from code alone.

See how it works.

Conclusion

The @MainActor attribute ensures your code performs on the main thread, which is essential for updating UI in your apps. You can apply it to properties, methods, instances, and closures, and since Swift 6.2, new projects even apply it by default. Methods like MainActor.run

and assumeIsolated

give you direct control in cases where annotations don’t fit.

Isolation is one of those concepts that only clicks once you’ve seen it applied across a real project. That’s exactly what I do in my Swift Concurrency Course, where you’ll get confident with actors and isolation before starting your Swift 6 migration.

If you like to learn more tips on Swift, check out the Swift category page. Feel free to contact me or tweet me on Twitter if you have any additional tips or feedback.

Thanks!

── more in #developer-tools 4 stories · sorted by recency
── more on @apple 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
Live at https://your-agent.zahid.host
Get free account → Pricing
from €0/mo · no card required
LIVE [news/mainactor-in-swift-e…] indexed:0 read:10min 2026-08-02 ·