@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.
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.
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!