Article by Pushkar Deshmukh

Swift Actors in iOS Architecture: The Future of Thread Safety

Thread safety in iOS apps has traditionally relied on DispatchQueue, locks, and serial queues. Swift introduced Actors to solve this problem at the language level. This article walks through the evolution of thread safety in Swift using simple examples and shows how Actors simplify concurrency in modern iOS architectures.

Pushkar Deshmukh

Pushkar Deshmukh

Senior iOS Engineer

March 14, 20264 min read11 views
SwiftSwift ConcurrencySwift ActorsiOS DevelopmentThread SafetyiOS ArchitectureSwiftUIUIKitMobile Engineering
Swift Actors in iOS Architecture: The Future of Thread Safety

The Problem: Shared Mutable State

Modern iOS apps execute many tasks concurrently:

  • network requests

  • background processing

  • caching

  • database operations

When multiple threads modify the same data, we risk data races.

Example:

var count = 0

DispatchQueue.global().async {
    count += 1
}

DispatchQueue.global().async {
    count += 1
}

Both tasks might update count simultaneously, producing unpredictable results.

To solve this, developers historically relied on several techniques.


1️⃣ Thread Safety Using DispatchQueue

A common solution is protecting state with a queue.

class CounterStore {

    private var count = 0
    private let queue = DispatchQueue(label: "counter.queue")

    func increment() {
        queue.sync {
            count += 1
        }
    }

    func getCount() -> Int {
        queue.sync {
            count
        }
    }
}

Usage:

let store = CounterStore()

DispatchQueue.global().async {
    store.increment()
}

DispatchQueue.global().async {
    store.increment()
}

The queue ensures only one thread modifies the state at a time.

⚠️ Problem:
Thread safety relies on always accessing the state through the queue.

The compiler cannot enforce this.


2️⃣ Thread Safety Using NSLock

Another traditional approach is explicit locking.

class CounterStore {

    private var count = 0
    private let lock = NSLock()

    func increment() {
        lock.lock()
        count += 1
        lock.unlock()
    }

    func getCount() -> Int {
        lock.lock()
        let value = count
        lock.unlock()
        return value
    }
}

Locks guarantee that only one thread accesses the state at a time.

⚠️ Downsides:

  • easy to forget unlock()

  • risk of deadlocks

  • harder to maintain in complex systems


3️⃣ Serial Queues

A serial queue executes tasks one at a time in order.

class CounterStore {

    private var count = 0
    private let queue = DispatchQueue(label: "counter.serial")

    func increment() {
        queue.async {
            self.count += 1
        }
    }

    func getCount(completion: @escaping (Int) -> Void) {
        queue.async {
            completion(self.count)
        }
    }
}

Serial queues ensure that mutations happen sequentially, preventing race conditions.

⚠️ Downsides:

  • introduces callback-based APIs

  • safety still relies on discipline

  • direct state access can still break safety


4️⃣ Concurrent Queues

Concurrent queues allow multiple tasks to run simultaneously.

let queue = DispatchQueue.global()

queue.async {
    print("Task 1")
}

queue.async {
    print("Task 2")
}

Execution order is not guaranteed, and tasks may run in parallel.

If shared state is modified:

var count = 0

DispatchQueue.global().async {
    count += 1
}

DispatchQueue.global().async {
    count += 1
}

This creates a race condition.


Concurrent Queue + Barrier Pattern

To improve performance in read-heavy systems, developers sometimes use barriers.

class CounterStore {

    private var count = 0
    private let queue = DispatchQueue(
        label: "counter.concurrent",
        attributes: .concurrent
    )

    func increment() {
        queue.async(flags: .barrier) {
            self.count += 1
        }
    }

    func getCount(completion: @escaping (Int) -> Void) {
        queue.async {
            completion(self.count)
        }
    }
}

How it works:

  • multiple reads run concurrently

  • writes are exclusive due to the barrier

⚠️ Still, safety depends on developers always using the queue correctly.


The Core Problem

All these techniques share one issue:

Thread safety depends on developer discipline.

The compiler cannot enforce correct usage.

This is exactly what Swift Actors solve.


5️⃣ Swift Actors: Thread Safety by Design

Swift introduced Actors to provide built-in isolation for mutable state.

actor CounterStore {

    private var count = 0

    func increment() {
        count += 1
    }

    func getCount() -> Int {
        count
    }
}

Usage:

let store = CounterStore()

Task {
    await store.increment()
}

Task {
    let value = await store.getCount()
    print(value)
}

Actors guarantee:

  • only one task accesses state at a time

  • no locks required

  • no queues required

Thread safety becomes a language-level guarantee.


Real iOS Example: Thread-Safe Cache

Actors work extremely well for shared services like caching.

actor ImageCache {

    private var storage: [String: Data] = [:]

    func save(_ data: Data, for key: String) {
        storage[key] = data
    }

    func get(for key: String) -> Data? {
        storage[key]
    }
}

Usage:

let cache = ImageCache()

Task {
    await cache.save(imageData, for: "profile")
}

Task {
    let data = await cache.get(for: "profile")
}

Even if multiple tasks access the cache concurrently, the actor guarantees safe access.


Actors in Real iOS Architecture

Actors become extremely powerful in the data layer.

Typical use cases:

  • repositories

  • caching systems

  • session management

  • shared services

Example repository:

actor UserRepository {

    private var cachedUser: User?

    func fetchUser() async throws -> User {

        if let user = cachedUser {
            return user
        }

        let user = try await APIClient.fetchUser()

        cachedUser = user
        return user
    }
}

This repository becomes naturally thread-safe.

No locks or queues are needed.


UI Layer: Using MainActor

UI updates must happen on the main thread.

Swift provides a special actor for this:

@MainActor

Example ViewModel:

@MainActor
class ProfileViewModel {

    private let repository = UserRepository()

    func loadUser() async throws -> User {
        try await repository.fetchUser()
    }
}

Architecture becomes clear:

Actors → shared background state
MainActor → UI state

Evolution of Thread Safety in Swift

Approach

Thread Safety

Complexity

Compiler Enforcement

NSLock

Medium

High

DispatchQueue

Medium

Medium

Serial Queue

Medium

Medium

Concurrent + Barrier

Medium

High

Swift Actors

High

Low

Actors move concurrency safety from developer discipline → language guarantees.


Final Thoughts

Concurrency bugs are among the hardest problems to debug in production systems.

Swift Actors simplify this dramatically.

Instead of protecting shared state with locks and queues, we now isolate state by design.

For modern Swift concurrency architectures, Actors are quickly becoming a core building block for safe and scalable iOS systems.


Pushkar Deshmukh

Written by

Pushkar Deshmukh

Senior iOS Engineer

11+ years of experience building mobile and web applications. Passionate about Swift, React, and sharing knowledge through technical writing.

11views0comments

Comments

Loading…

Loading comments…

Read next

View all
Back to All Articles