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:
- Cross-Type Coordination: When multiple classes need to work with the same shared resource
- Thread Affinity: Ensuring certain code always runs on specific threads (like UI on main thread)
- Domain Isolation: Keeping different subsystems (networking, database, analytics) properly synchronized
- Compile-Time Safety: Moving thread-safety from runtime checks to compile-time guarantees
Creating a Global Actor
To create a global actor, you need:
- The
@globalActor
attribute - A shared static instance
- 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
-
Use Meaningful Names: Name your global actors based on their purpose (DatabaseActor, NetworkActor, etc.)
-
Keep Global Actors Focused: Each global actor should have a single, clear responsibility
-
Minimize Cross-Actor Communication: Frequent switching between actors impacts performance
-
Use @MainActor for UI: Always use @MainActor for UI updates rather than creating custom UI actors
-
Consider Performance: Global actors serialize access – only use when synchronization is needed
Common Pitfalls to Avoid
-
Over-using Global Actors: Don’t mark everything with a global actor – only use when you need synchronization
-
Blocking Operations: Avoid long-running synchronous operations in global actors as they block other operations
-
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.