Global Actors in Swift iOS



This content originally appeared on DEV Community and was authored by ArshTechPro

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.


This content originally appeared on DEV Community and was authored by ArshTechPro