Swift Actors

Thread-safe data isolation in concurrent Swift

🎭 What are Swift Actors?

Actors are reference types that protect their data from concurrent access. They ensure only one task can access their mutable state at a time, preventing data races and making concurrent programming safer.


// Simple actor example
actor Counter {
    private var value = 0
    
    func increment() {
        value += 1
    }
    
    func getValue() -> Int {
        return value
    }
}

// Usage
let counter = Counter()
await counter.increment() // Safe concurrent access
let currentValue = await counter.getValue()
                                    

Key Actor Concepts

🔒

Data Isolation

Actors protect their internal state

actor BankAccount {
    private var balance: Double = 0
    
    func deposit(_ amount: Double) {
        balance += amount
    }
}

Async Access

Actor methods are called with await

let account = BankAccount()
await account.deposit(100.0)
🚫

No Data Races

Prevents concurrent access issues

// Multiple tasks can safely access
Task { await counter.increment() }
Task { await counter.increment() }
🎯

MainActor

Special actor for UI updates

@MainActor
class ViewModel: ObservableObject {
    @Published var data = ""
}

🔹 Creating Basic Actors

Define actors to protect shared data:

actor TemperatureSensor {
    private var readings: [Double] = []
    private var currentTemp: Double = 20.0
    
    func addReading(_ temperature: Double) {
        readings.append(temperature)
        currentTemp = temperature
    }
    
    func getCurrentTemperature() -> Double {
        return currentTemp
    }
    
    func getAverageTemperature() -> Double {
        guard !readings.isEmpty else { return 0 }
        return readings.reduce(0, +) / Double(readings.count)
    }
    
    func getReadingCount() -> Int {
        return readings.count
    }
}

// Usage
let sensor = TemperatureSensor()

Task {
    await sensor.addReading(25.5)
    await sensor.addReading(23.2)
    
    let current = await sensor.getCurrentTemperature()
    let average = await sensor.getAverageTemperature()
    
    print("Current: \(current)°C, Average: \(average)°C")
}

🔹 Actor Isolation

Understanding how actors protect their data:

actor FileManager {
    private var files: [String: Data] = [:]
    
    // This method is isolated to the actor
    func saveFile(name: String, data: Data) {
        files[name] = data
        print("Saved file: \(name)")
    }
    
    // This method is also isolated
    func loadFile(name: String) -> Data? {
        return files[name]
    }
    
    // Nonisolated methods don't need await
    nonisolated func getFileExtension(filename: String) -> String {
        return String(filename.split(separator: ".").last ?? "")
    }
}

let fileManager = FileManager()

// These need await (isolated methods)
await fileManager.saveFile(name: "doc.txt", data: Data())
let data = await fileManager.loadFile(name: "doc.txt")

// This doesn't need await (nonisolated)
let ext = fileManager.getFileExtension(filename: "doc.txt")

🔹 MainActor for UI

Use MainActor to ensure UI updates happen on the main thread:

import SwiftUI

@MainActor
class DataStore: ObservableObject {
    @Published var items: [String] = []
    @Published var isLoading = false
    
    func loadItems() async {
        isLoading = true
        
        // Background work
        let newItems = await fetchItemsFromAPI()
        
        // UI updates automatically on main thread
        items = newItems
        isLoading = false
    }
    
    func addItem(_ item: String) {
        items.append(item)
    }
}

// In SwiftUI View
struct ItemListView: View {
    @StateObject private var store = DataStore()
    
    var body: some View {
        NavigationView {
            List(store.items, id: \.self) { item in
                Text(item)
            }
            .navigationTitle("Items")
            .task {
                await store.loadItems()
            }
        }
    }
}

🔹 Actor Inheritance and Protocols

Actors can conform to protocols but cannot inherit from classes:

protocol DataProvider {
    func fetchData() async -> [String]
}

actor NetworkDataProvider: DataProvider {
    private let baseURL: String
    
    init(baseURL: String) {
        self.baseURL = baseURL
    }
    
    func fetchData() async -> [String] {
        // Simulate network request
        try? await Task.sleep(nanoseconds: 1_000_000_000)
        return ["Item 1", "Item 2", "Item 3"]
    }
}

actor CacheDataProvider: DataProvider {
    private var cachedData: [String] = []
    
    func fetchData() async -> [String] {
        if cachedData.isEmpty {
            cachedData = ["Cached Item 1", "Cached Item 2"]
        }
        return cachedData
    }
    
    func clearCache() {
        cachedData.removeAll()
    }
}

// Usage
let networkProvider = NetworkDataProvider(baseURL: "https://api.example.com")
let cacheProvider = CacheDataProvider()

let networkData = await networkProvider.fetchData()
let cachedData = await cacheProvider.fetchData()

🔹 Actor Best Practices

Guidelines for effective actor usage:

✅ Do:

  • Use actors for shared mutable state
  • Keep actor methods focused and avoid long-running operations
  • Use @MainActor for UI-related classes
  • Make methods nonisolated when they don't access mutable state
  • Group related data in the same actor

❌ Don't:

  • Use actors for simple value types
  • Access actor properties directly from outside
  • Create too many small actors - group related functionality
  • Block actor methods with synchronous operations

🧠 Test Your Knowledge

How do you call a method on an actor?