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