DEV Community

ArshTechPro
ArshTechPro

Posted on

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) } 
Enter fullscreen mode Exit fullscreen mode

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() {} } 
Enter fullscreen mode Exit fullscreen mode

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)") } } } 
Enter fullscreen mode Exit fullscreen mode

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") } } 
Enter fullscreen mode Exit fullscreen mode

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 } } 
Enter fullscreen mode Exit fullscreen mode

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") } 
Enter fullscreen mode Exit fullscreen mode

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 
Enter fullscreen mode Exit fullscreen mode

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.

Top comments (1)

Collapse
 
arshtechpro profile image
ArshTechPro

Global actors extend the actor concept to provide app-wide synchronization domains.