Swift Concurrency: Actors & Data Safety
Part 1: Basics - Synchronous code, GCD, and async/await fundamentals.
Part 2: Parallel Execution - Running independent tasks concurrently.
Part 3: Errors & Cancellation - Propagation and task cancellation.
Part 4: Actors - Protecting shared state with actor isolation.
- XC Project - Download the project with all examples used in this series
Actors are reference types in Swift Concurrency that protect shared state by ensuring only one task can access their data at a time. They prevent data races without manual synchronization.
Why Use Actors:
Thread Safety: Access to properties and methods is automatically serialized.
Simpler Code: No locks, semaphores, or manual synchronization required.
Built-in Concurrency: Works seamlessly with async/await and Tasks.
How Actors Work:
Actors isolate their state: properties and methods can only be accessed from within the actor or via await.
Calls from outside are queued and executed one at a time, ensuring safe concurrent access.
In a shop scenario, multiple tasks try to claim items from stock concurrently. A naive class (UnsafeStock) allows race conditions, causing the remaining stock to be unpredictable. Using an actor (SafeStock) guarantees thread-safe access, so the stock is correctly decremented every time.
class Shop { let stock: UnsafeStock let safeStock: SafeStock let stockSize: Int init(stockSize: Int = 2000) { self.stockSize = stockSize self.stock = UnsafeStock(value: stockSize) self.safeStock = SafeStock(value: stockSize) } func claimEntireStock() async { await withTaskGroup(of: Void.self) { group in for _ in 1...stockSize { group.addTask { await self.stock.claimItem() await self.safeStock.claimItem() } } } print("Unsafe: \(stock.value), Safe: \(await safeStock.value)") } }
class UnsafeStock { var value: Int init(value: Int) { self.value = value } func claimItem() async { value -= 1 } }
actor SafeStock { var value: Int init(value: Int) { self.value = value } func claimItem() async { value -= 1 } }
In this example, multiple tasks try to claim items from the shop’s stock simultaneously. Running the same code multiple times produced the following results:
- Unsafe: 25, Safe: 0
- Unsafe: 3, Safe: 0
- Unsafe: 6, Safe: 0
- Unsafe: 1, Safe: 0
- Unsafe: 4, Safe: 0
The UnsafeStock class is not thread-safe, so its remaining stock varies each run due to race conditions. Multiple tasks may read and write the same value at the same time, causing lost updates and unpredictable results.
The SafeStock actor serializes access automatically, so its remaining stock is always correct (0 in this case), no matter how many tasks run concurrently.
This demonstrates what actors solve in real-world code: protecting shared mutable state without manual synchronization, locks, or complex concurrency management.
MainActor is a global actor that represents the main thread.
Anything isolated to @MainActor is guaranteed to execute on the main thread, which is required for UI updates and UI interaction.
With MainActor, Swift concurrency lets you declare that certain code must always run on the main thread.
Actor isolation means that an actor (or a @MainActor-annotated type) owns its state and guarantees that only one task can access or mutate that state at a time. Any access from outside its isolation context must use await, and Swift automatically schedules that work so it runs safely and sequentially.
In practice, this gives you thread safety by default, without locks or manual synchronization.
nonisolated explicitly opts a method or property out of the actor’s isolation. This allows the code to run concurrently and without automatically hopping onto the actor, but it also means the method cannot directly access isolated state.
This is commonly used for:
CPU or network work that doesn’t touch actor state
Performance-sensitive code
Clearly separating background work from UI updates
The compiler enforces these boundaries, making isolation rules visible and hard to misuse.
This example shows three ways of combining @MainActor, actor isolation, and nonisolated work when loading data and updating UI state.
Each tab performs the same logical operation, but differs in where the work executes and how UI updates are isolated, which has a direct impact on responsiveness and correctness.
@MainActor final class ImageViewModel { var image: UIImage? func loadImage() async { let url = URL(string: "https://zhivkomanchev.com/wp-content/uploads/2026/02/zm-logo.png")! let (data, _) = try! await URLSession.shared.data(from: url) let image = UIImage(data: data) self.image = image } }
@MainActor final class ImageViewModel { var image: UIImage? nonisolated func loadImage() async { let url = URL(string: "https://zhivkomanchev.com/wp-content/uploads/2026/02/zm-logo.png")! let (data, _) = try! await URLSession.shared.data(from: url) let image = UIImage(data: data) await MainActor.run { self.image = image } } }
final class ImageViewModel { @MainActor var image: UIImage? func loadImage() async { let url = URL(string: "https://zhivkomanchev.com/wp-content/uploads/2026/02/zm-logo.png")! let (data, _) = try! await URLSession.shared.data(from: url) let image = UIImage(data: data) await MainActor.run { self.image = image } } }
- @MainActor Class
The entire class is isolated to the main actor, including the method that performs the work.
This means all execution happens on the main thread, which can block UI updates and cause visible freezes when doing non-trivial work. - nonisolated Work
The class remains @MainActor, but the heavy work is moved into a nonisolated method.
Only the UI update hops back to the main actor, keeping the interface responsive while still guaranteeing safe UI access. - @MainActor Property
The class itself is not main-actor-isolated.
Only the UI-bound property is marked @MainActor, allowing background work to run freely while enforcing that UI state updates occur on the main thread. - Together, these cases illustrate how actor isolation is not all-or-nothing, and how Swift Concurrency lets you precisely control what runs on the main actor and why.
Support This Content
If you found this helpful, you can buy me a coffee here.
Swift Concurrency Series Part 1: Basics – Synchronous code, GCD, and async/await fundamentals. Part 2: Parallel Execution – Running independent…
Swift Concurrency Series Part 1: Basics – Synchronous code, GCD, and async/await fundamentals. Part 2: Parallel Execution – Running independent…