Understanding Actors in Swift
Published on · Updated · 21 min
Actors are at the heart of Swift's concurrency model. Introduced with Swift 5.5, they elegantly solve the data race problem by isolating mutable state. With Swift 6.2, their usage becomes even more intuitive thanks to MainActor by default. This guide covers everything you need to know to master them.
The Data Race Problem
Before Actors, protecting shared state required locks, queues, or semaphores. It was verbose and error-prone:
This code works, but the compiler cannot verify that you haven't forgotten to use the queue somewhere. A single omission is enough to create a data race.
Actor: Compiler-Guaranteed Isolation
An Actor is a reference type that automatically isolates its state. The compiler guarantees that only one call can access the state at a time:
No more queues, no more locks. The compiler takes care of everything. Each Actor maintains an internal queue (executor) that serializes access.
Accessing Actors: await Required
From outside an Actor, every access to an isolated property or method requires await:
The await keyword indicates a potential suspension point. Your code waits its turn in the Actor's queue before accessing the state.
From Inside the Actor
Inside the Actor, you access properties and methods directly without await:
nonisolated: Opting Out of Isolation
Some properties or methods don't need isolation. Use nonisolated to exempt them:
Constants declared with let are automatically nonisolated because they are immutable and therefore thread-safe.
nonisolated for Protocol Conformances
A common case: conforming an Actor to a synchronous protocol like CustomStringConvertible:
MainActor: The Main Thread Actor
MainActor is a predefined global actor that represents the main thread. All UI code must run on it:
With @MainActor on the class, all properties and methods execute on the main thread. @Published properties can be modified safely.
MainActor.run for One-Off Blocks
If you only have a small portion of code to execute on MainActor:
MainActor.assumeIsolated for Callbacks
When you know a callback will be called on the main thread but the compiler can't verify it:
Warning: assumeIsolated crashes if you're not actually on MainActor at runtime.
Custom Global Actors
You can create your own global actors to isolate specific resources:
All accesses to FileManager are now serialized on DiskActor, avoiding file access conflicts.
When to Use Global Actor vs Standard Actor
| Situation | Recommended Choice |
|---|---|
Single shared instance | Global Actor |
Multiple independent instances | Standard Actor |
UI coordination | MainActor |
Unique system resource (DB, files) | Custom Global Actor |
Feature-local state | Standard Actor |
Sendable: Crossing Isolation Boundaries
When data crosses Actor boundaries, it must be Sendable. The Sendable protocol guarantees that a type can be shared between concurrent contexts without data races.
Automatically Sendable Types
Some types are Sendable by default:
Explicit Conformance for Classes
Classes require special attention:
@unchecked Sendable: Use with Caution
For types with internal synchronization that the compiler can't verify:
Warning: @unchecked Sendable disables compiler checks. You are responsible for thread-safety. Prefer Mutex (iOS 18+) when possible:
@Sendable for Closures
Closures passed between concurrent contexts must be @Sendable:
Swift 6.2: MainActor by Default
Swift 6.2 introduces defaultIsolation which fundamentally changes the approach:
With this setting, all code without explicit annotation runs on MainActor:
@concurrent for Parallel Work
If you want to explicitly leave MainActor:
Isolated Conformances (SE-0470)
Swift 6.2 allows restricting a protocol conformance to an actor:
Without @MainActor Equatable, the compiler could execute == on any thread, violating the class isolation.
Actor Reentrancy
A crucial point: Actors are reentrant. When you await inside an Actor, other calls can execute while waiting:
Pattern to Avoid Reentrancy Problems
Capture necessary values before await:
Or use a transactional approach:
Architecture Patterns with Actors
Repository Actor
ViewModel with Internal Actor
Best Practices
1. Prefer Actors Over Manual Locks
2. Minimize awaits Inside Actors
Each await is a point where state can change. Group operations together:
3. Keep Actors Focused
An Actor should have a single responsibility:
Common Pitfalls
1. Excessive Actor Hopping
Switching between Actors has a cost:
2. Deadlocks with Synchronous Calls
Actors only have async methods from the outside. Watch out for designs that create circular dependencies:
3. Forgetting Computed Properties Are Isolated
Going Further
Actors are the foundation of safe concurrency in Swift. Master them before moving on to advanced concepts:
TaskGroup for dynamic parallelism
AsyncStream for asynchronous data flows
Custom Executors to control execution