.agents/skills/swift-concurrency/references/actors.md
Use this when:
actor, @MainActor, nonisolated, or Mutex.Skip this file if:
sendable.md.threading.md.Jump to:
#isolation MacroActors protect mutable state by ensuring only one task accesses it at a time. They're reference types with automatic synchronization.
actor Counter {
var value = 0
func increment() {
value += 1
}
}
Key guarantee: Only one task can access mutable state at a time (serialized access).
Course Deep Dive: This topic is covered in detail in Lesson 5.1: Understanding actors in Swift Concurrency
actor BankAccount {
var balance: Int = 0
func deposit(_ amount: Int) {
balance += amount
}
}
let account = BankAccount()
account.balance += 1 // ❌ Error: can't mutate from outside
await account.deposit(1) // ✅ Must use actor's methods
let account = BankAccount()
await account.deposit(100)
print(await account.balance) // Must await reads too
Always use await when accessing actor properties/methods—you don't know if another task is inside.
NSObject for Objective-C interop)// ❌ Can't inherit from actors
actor Base {}
actor Child: Base {} // Error
// ✅ NSObject exception
actor Example: NSObject {} // OK for Objective-C
Shared isolation domain across types, functions, and properties.
Ensures execution on main thread:
@MainActor
final class ViewModel {
var items: [Item] = []
}
@MainActor
func updateUI() {
// Always runs on main thread
}
@MainActor
var title: String = ""
@globalActor
actor ImageProcessing {
static let shared = ImageProcessing()
private init() {} // Prevent duplicate instances
}
@ImageProcessing
final class ImageCache {
var images: [URL: Data] = [:]
}
@ImageProcessing
func applyFilter(_ image: UIImage) -> UIImage {
// All image processing serialized
}
Use private init to prevent creating multiple executors.
Course Deep Dive: This topic is covered in detail in Lesson 5.2: An introduction to Global Actors
UI-related code that must run on main thread:
@MainActor
final class ContentViewModel: ObservableObject {
@Published var items: [Item] = []
}
// Old way
DispatchQueue.main.async {
// Update UI
}
// Modern way
await MainActor.run {
// Update UI
}
// Better: Use attribute
@MainActor
func updateUI() {
// Automatically on main thread
}
Use sparingly - assumes you're on main thread, crashes if not:
func methodB() {
assert(Thread.isMainThread) // Validate assumption
MainActor.assumeIsolated {
someMainActorMethod()
}
}
Prefer: Explicit @MainActor or await MainActor.run over assumeIsolated.
Course Deep Dive: This topic is covered in detail in Lesson 5.3: When and how to use @MainActor
Actor methods are isolated by default:
actor BankAccount {
var balance: Double
// Implicitly isolated
func deposit(_ amount: Double) {
balance += amount
}
}
Reduce suspension points by inheriting caller's isolation:
struct Charger {
static func charge(
amount: Double,
from account: isolated BankAccount
) async throws -> Double {
// No await needed - we're isolated to account
try account.withdraw(amount: amount)
return account.balance
}
}
actor Database {
func transaction<T>(
_ operation: @Sendable (_ db: isolated Database) throws -> T
) throws -> T {
beginTransaction()
let result = try operation(self)
commitTransaction()
return result
}
}
// Usage: Multiple operations, one await
try await database.transaction { db in
db.insert(item1)
db.insert(item2)
db.insert(item3)
}
extension Actor {
func performInIsolation<T: Sendable>(
_ block: @Sendable (_ actor: isolated Self) throws -> T
) async rethrows -> T {
try block(self)
}
}
// Usage
try await bankAccount.performInIsolation { account in
try account.withdraw(amount: 20)
print("Balance: \(account.balance)")
}
Opt out of isolation for immutable data:
actor BankAccount {
let accountHolder: String
nonisolated var details: String {
"Account: \(accountHolder)"
}
}
// No await needed
print(account.details)
extension BankAccount: CustomStringConvertible {
nonisolated var description: String {
"Account: \(accountHolder)"
}
}
Course Deep Dive: This topic is covered in detail in Lesson 5.4: Isolated vs. non-isolated access in actors
Clean up actor state on deallocation:
actor FileDownloader {
var downloadTask: Task<Void, Error>?
isolated deinit {
downloadTask?.cancel() // Can call isolated methods
}
}
Requires: iOS 18.4+, macOS 15.4+
Course Deep Dive: This topic is covered in detail in Lesson 5.5: Using Isolated synchronous deinit
Protocol conformance respecting actor isolation:
@MainActor
final class PersonViewModel {
let id: UUID
var name: String
}
extension PersonViewModel: @MainActor Equatable {
static func == (lhs: PersonViewModel, rhs: PersonViewModel) -> Bool {
lhs.id == rhs.id && lhs.name == rhs.name
}
}
Enable: InferIsolatedConformances upcoming feature.
Course Deep Dive: This topic is covered in detail in Lesson 5.6: Adding isolated conformance to protocols
Critical: State can change between suspension points.
actor BankAccount {
var balance: Double
func deposit(amount: Double) async {
balance += amount
// ⚠️ Actor unlocked during await
await logActivity("Deposited \(amount)")
// ⚠️ Balance may have changed!
print("Balance: \(balance)")
}
}
async let _ = account.deposit(50)
async let _ = account.deposit(50)
async let _ = account.deposit(50)
// May print same balance three times:
// Balance: 150
// Balance: 150
// Balance: 150
Complete actor work before suspending:
func deposit(amount: Double) async {
balance += amount
print("Balance: \(balance)") // Before suspension
await logActivity("Deposited \(amount)")
}
Rule: Don't assume state is unchanged after await.
Course Deep Dive: This topic is covered in detail in Lesson 5.7: Understanding actor reentrancy
Inherit caller's isolation for generic code:
extension Collection where Element: Sendable {
func sequentialMap<Result: Sendable>(
isolation: isolated (any Actor)? = #isolation,
transform: (Element) async -> Result
) async -> [Result] {
var results: [Result] = []
for element in self {
results.append(await transform(element))
}
return results
}
}
// Usage from @MainActor context
Task { @MainActor in
let names = ["Alice", "Bob"]
let results = await names.sequentialMap { name in
await process(name) // Inherits @MainActor
}
}
Benefits: Avoids unnecessary suspensions, allows non-Sendable data.
When spawning unstructured Task closures that need to work with non-Sendable types, you must capture the isolation parameter to inherit the caller's isolation context.
Problem: Task closures are @Sendable, which prevents capturing non-Sendable types:
func process(delegate: NonSendableDelegate) {
Task {
delegate.doWork() // ❌ Error: capturing non-Sendable type
}
}
Solution: Use #isolation parameter and capture it inside the Task:
func process(
delegate: NonSendableDelegate,
isolation: isolated (any Actor)? = #isolation
) {
Task {
_ = isolation // Forces capture, Task inherits caller's isolation
delegate.doWork() // ✅ Safe - running on caller's actor
}
}
Why _ = isolation is required: Per SE-0420, Task closures only inherit isolation when "a non-optional binding of an isolated parameter is captured by the closure." The _ = isolation statement forces this capture. The capture list syntax [isolation] should work but currently does not.
When to use this pattern:
Tasks that work with non-Sendable delegate objectsNote: This pattern keeps the non-Sendable value alive and accessible within the Task. The Task runs on the caller's isolation domain, so no cross-isolation "sending" occurs.
Course Deep Dive: This topic is covered in detail in Lesson 5.8: Inheritance of actor isolation using the #isolation macro
Advanced: Control how actor schedules work.
final class DispatchQueueExecutor: SerialExecutor {
private let queue: DispatchQueue
init(queue: DispatchQueue) {
self.queue = queue
}
func enqueue(_ job: consuming ExecutorJob) {
let unownedJob = UnownedJob(job)
let executor = asUnownedSerialExecutor()
queue.async {
unownedJob.runSynchronously(on: executor)
}
}
}
actor LoggingActor {
private let executor: DispatchQueueExecutor
nonisolated var unownedExecutor: UnownedSerialExecutor {
executor.asUnownedSerialExecutor()
}
init(queue: DispatchQueue) {
executor = DispatchQueueExecutor(queue: queue)
}
}
Default executor is usually sufficient.
Course Deep Dive: This topic is covered in detail in Lesson 5.9: Using a custom actor executor
Synchronous locking without async/await overhead (iOS 18+, macOS 15+).
import Synchronization
final class Counter {
private let count = Mutex<Int>(0)
var currentCount: Int {
count.withLock { $0 }
}
func increment() {
count.withLock { $0 += 1 }
}
}
final class TouchesCapturer: Sendable {
let path = Mutex<NSBezierPath>(NSBezierPath())
func storeTouch(_ point: NSPoint) {
path.withLock { path in
path.move(to: point)
}
}
}
func decrement() throws {
try count.withLock { count in
guard count > 0 else {
throw Error.reachedZero
}
count -= 1
}
}
| Feature | Mutex | Actor |
|---|---|---|
| Synchronous | ✅ | ❌ (requires await) |
| Async support | ❌ | ✅ |
| Thread blocking | ✅ | ❌ (cooperative) |
| Fine-grained locking | ✅ | ❌ (whole actor) |
| Legacy code integration | ✅ | ❌ |
Use Mutex when:
Use Actor when:
Course Deep Dive: This topic is covered in detail in Lesson 5.10: Using a Mutex as an alternative to actors
@MainActor
final class ContentViewModel: ObservableObject {
@Published var items: [Item] = []
func loadItems() async {
items = try await api.fetchItems()
}
}
@ImageProcessing
final class ImageProcessor {
func process(_ images: [UIImage]) async -> [UIImage] {
images.map { applyFilters($0) }
}
}
actor DataStore {
private var items: [Item] = []
func add(_ item: Item) {
items.append(item)
}
nonisolated func itemCount() -> Int {
// ❌ Can't access items
return 0
}
}
actor Database {
func transaction<T>(
_ operation: @Sendable (_ db: isolated Database) throws -> T
) throws -> T {
beginTransaction()
defer { commitTransaction() }
return try operation(self)
}
}
Need thread-safe mutable state?
├─ Async context?
│ ├─ Single instance? → Actor
│ ├─ Global/shared? → Global Actor (@MainActor, custom)
│ └─ UI-related? → @MainActor
│
└─ Synchronous context?
├─ Can refactor to async? → Actor
├─ Legacy code integration? → Mutex
└─ Fine-grained locking? → Mutex
For migration strategies, advanced patterns, and real-world examples, see Swift Concurrency Course.