Global Actors in Swift iOS

How Actors Work

An actor processes one request at a time in a serialized manner. When you call an actor’s method from outside its isolation context, you must use await, which creates a suspension point where your code waits for its turn to execute.

actor DownloadManager {
    private var activeDownloads: [URL: Progress] = [:]
    private var completedFiles: [URL: Data] = [:]

    func startDownload(from url: URL) -> String {
        if activeDownloads[url] != nil {
            return "Download already in progress"
        }

        let progress = Progress()
        activeDownloads[url] = progress
        return "Download started"
    }

    func completeDownload(url: URL, data: Data) {
        activeDownloads.removeValue(forKey: url)
        completedFiles[url] = data
    }

    func getCompletedData(for url: URL) -> Data? {
        return completedFiles[url]
    }
}

// Usage requires await
func handleDownload() async {
    let manager = DownloadManager()
    let status = await manager.startDownload(from: URL(string: "https://example.com/file")!)
    print(status)
}

For details about MainActor refer below article
https://dev.to/arshtechpro/understanding-mainactor-in-swift-when-and-how-to-use-it-4ii4

What are Global Actors?

Global actors are a powerful Swift concurrency feature that extend the actor model to provide app-wide synchronization domains. While regular actors protect individual instances, global actors ensure that multiple pieces of code across different types and modules execute on the same serialized executor.

Think of a global actor as a synchronization coordinator that manages access to shared resources across your entire application. The most familiar example is @MainActor, which ensures code runs on the main thread.

Why Do We Need Global Actors?

Global actors solve specific synchronization challenges:

  1. Cross-Type Coordination: When multiple classes need to work with the same shared resource
  2. Thread Affinity: Ensuring certain code always runs on specific threads (like UI on main thread)
  3. Domain Isolation: Keeping different subsystems (networking, database, analytics) properly synchronized
  4. Compile-Time Safety: Moving thread-safety from runtime checks to compile-time guarantees

Creating a Global Actor

To create a global actor, you need:

  1. The @globalActor attribute
  2. A shared static instance
  3. The actor keyword

Here’s the basic structure:

@globalActor
actor MyCustomActor {
    static let shared = MyCustomActor()
    private init() {}
}

How to Use Global Actors

Global actors can be applied at three levels:

1. Class-Level Application

When you mark an entire class with a global actor, all its properties and methods become part of that actor’s domain:

@globalActor
actor DatabaseActor {
    static let shared = DatabaseActor()
    private init() {}
}

@DatabaseActor
class DatabaseManager {
    private var cache: [String: Any] = [:]
    private var transactionCount = 0

    func save(key: String, value: Any) {
        cache[key] = value
        transactionCount += 1
        print("Saved: (key) - Total transactions: (transactionCount)")
    }

    func retrieve(key: String) -> Any? {
        return cache[key]
    }

    func clearCache() {
        cache.removeAll()
        print("Cache cleared")
    }
}

// Usage
class ViewController: UIViewController {
    let database = DatabaseManager()

    func saveUserData() async {
        // Must use await - accessing DatabaseActor from outside
        await database.save(key: "username", value: "John")
        await database.save(key: "lastLogin", value: Date())

        // All these calls are synchronized - no race conditions
        if let username = await database.retrieve(key: "username") {
            print("Retrieved: (username)")
        }
    }
}

2. Method-Level Application

You can mark specific methods to run on a global actor while keeping the rest of the class unaffected:

class DataService {
    private var localCache: [String: String] = []

    // Regular method - runs on any thread
    func processData(_ input: String) -> String {
        return input.uppercased()
    }

    // This method runs on DatabaseActor
    @DatabaseActor
    func syncToDatabase(_ data: String) {
        print("Syncing to database: (data)")
        // This is synchronized with all other DatabaseActor code
    }

    // This method runs on MainActor
    @MainActor
    func updateUI(with message: String) {
        // Safe to update UI here
        NotificationCenter.default.post(
            name: .dataUpdated,
            object: message
        )
    }

    func performCompleteSync() async {
        let processed = processData("hello world")
        await syncToDatabase(processed)
        await updateUI(with: "Sync complete")
    }
}

3. Property-Level Application

Individual properties can be bound to global actors:

class AppSettings {
    @DatabaseActor var userData: [String: Any] = [:]  // Bound to DatabaseActor
    @MainActor var currentTheme: String = "light"     // Bound to MainActor
    var cacheSize: Int = 100                          // Not actor-bound

    func updateSettings() async {
        // Need await for DatabaseActor property
        await DatabaseActor.run {
            userData["lastUpdate"] = Date()
        }

        // Need await for MainActor property
        await MainActor.run {
            currentTheme = "dark"
        }

        // No await needed for regular property
        cacheSize = 200
    }
}

Running Code on Global Actors

You can explicitly run code on a global actor using the run method:

// Run a closure on MainActor
await MainActor.run {
    // Update UI safely
    myLabel.text = "Updated"
}

// Run on your custom actor
await DatabaseActor.run {
    // This code runs on DatabaseActor
    print("Running database operation")
}

Nonisolated and Global Actors

You can opt specific members out of global actor isolation:

@DatabaseActor
class DataStore {
    private var records: [String: Any] = [:]
    let storeId = UUID()  // Immutable - safe to access

    // This property doesn't need synchronization
    nonisolated var debugDescription: String {
        return "DataStore: (storeId)"
    }

    // This method can be called without await
    nonisolated func validateKey(_ key: String) -> Bool {
        return !key.isEmpty && key.count < 100
    }

    // This needs synchronization - accesses mutable state
    func addRecord(key: String, value: Any) {
        records[key] = value
    }
}

// Usage
let store = DataStore()
print(store.debugDescription)  // No await needed
let isValid = store.validateKey("myKey")  // No await needed
await store.addRecord(key: "myKey", value: "data")  // Await required

Best Practices

  1. Use Meaningful Names: Name your global actors based on their purpose (DatabaseActor, NetworkActor, etc.)

  2. Keep Global Actors Focused: Each global actor should have a single, clear responsibility

  3. Minimize Cross-Actor Communication: Frequent switching between actors impacts performance

  4. Use @MainActor for UI: Always use @MainActor for UI updates rather than creating custom UI actors

  5. Consider Performance: Global actors serialize access – only use when synchronization is needed

Common Pitfalls to Avoid

  1. Over-using Global Actors: Don’t mark everything with a global actor – only use when you need synchronization

  2. Blocking Operations: Avoid long-running synchronous operations in global actors as they block other operations

  3. Circular Dependencies: Be careful when global actors call each other – this can lead to deadlocks

Summary

Global actors are a powerful tool for managing synchronization across your entire application. By understanding global actors, you can write safer concurrent code.

Similar Posts