Zhivko Manchev

Senior iOS Engineer

iOS Team Lead

Swift & SwiftUI Expert

Article

Swift Concurrency: Actors & Data Safety

February 5, 2025 Swift-Concurrency
Swift Concurrency Series
Actors: Definition & Usage

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.

Example 1. Unsafe vs Safe Stock

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
    }
}
Example overview

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

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

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.

Example 2. MainActor class with nonisolated work

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
        }
    }
}
Example overview
  • @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.

Tags:
Related Posts
Swift Concurrency: Errors & Cancellation

Swift Concurrency Series Part 1: Basics – Synchronous code, GCD, and async/await fundamentals. Part 2: Parallel Execution – Running independent…

Swift Concurrency: Parallel Tasks

Swift Concurrency Series Part 1: Basics – Synchronous code, GCD, and async/await fundamentals. Part 2: Parallel Execution – Running independent…

Write a comment