How to use Global Actors in Swift (@globalActor) | Modern Swift Concurrency #10
When multiple types across your codebase all need to share the same concurrency domain — not just the main thread, but any dedicated serial context — @globalActor lets you declare that domain once and apply it everywhere with a single annotation.
What You'll Learn
- What a global actor is and how it differs from a regular actor instance
- How
@MainActoris itself a global actor and how to create your own - How to annotate properties, methods, and entire types with a custom global actor
- How
nonisolatedlets you opt individual declarations out of a global actor's domain - How
MainActor.runlets you hop back to the main actor from inside an isolated context
Mental Model
Think of a global actor as a named, app-wide serial queue — like a dedicated server room that every team in the company must book before touching a specific set of machines. The room has exactly one key, and only one visitor can be inside at a time. Any file, any type, any function that needs to touch those machines must hold that key. The @globalActor attribute declares the room and names the key; @MyFirstGlobalActor stamped on a declaration says "this machine lives in that room."
@MainActor is the most familiar example: it is the dedicated server room for everything the user sees. What custom global actors add is the ability to define your own rooms. A database actor, a networking actor, or an analytics actor can each be a global actor — a single shared serial domain that any code in your app can reference by name without passing an actor instance around.
Detailed Explanation
A regular actor in Swift provides isolation for the data held by one specific instance. Two different MyActor instances are isolated from each other — there is no shared lock between them. A global actor is different: it is a singleton actor. Every caller that references @MyFirstGlobalActor is talking to the same, single underlying actor instance, no matter which file or module the caller lives in.
You declare a global actor with two pieces: the @globalActor attribute on a type, and a static shared property of an actor type. Swift uses that shared instance as the executor for everything annotated with your global actor. You can annotate whole classes, individual methods, or single stored properties — the granularity is up to you.
@MainActor is built into the Swift standard library as exactly this pattern. It provides a singleton actor whose executor is the main thread. Any declaration marked @MainActor — or any type conforming to MainActor isolation via @MainActor final class — will always execute its synchronous work on the main thread.
Custom global actors are useful when you have a category of work that must be serialized across your entire app but does not belong on the main thread. Heavy database reads, Core Data stack access, or audio session management are examples. Instead of threading an actor instance through every call site, you annotate the relevant types once and let the compiler enforce the isolation everywhere.
The nonisolated keyword lets a method or property step outside its type's global actor isolation. This is appropriate for pure computations, constants, or protocol conformances that genuinely do not touch isolated state. Without it, the compiler would require callers to await even a simple getter.
Code Structure
The sample is in GlobalActorBootcamp.swift and demonstrates the full lifecycle: declaring a custom global actor (MyFirstGlobalActor), backing it with an actor type (MyNewDataManager), applying the global actor annotation to a view model method, crossing back to @MainActor to publish UI state, and binding everything to a SwiftUI view.
Complete Code
GlobalActorBootcamp.swift
import SwiftUI
// @globalActor declares this type as a global actor.
// It must have a static `shared` property of an actor type —
// that actor instance becomes the executor for everything annotated @MyFirstGlobalActor.
@globalActor final class MyFirstGlobalActor {
// `shared` is the single, app-wide instance of the underlying actor.
// Swift routes all @MyFirstGlobalActor-annotated work through this actor's serial executor.
static var shared = MyNewDataManager()
}
// The underlying actor that actually provides the serial, isolated executor.
// Any method called on this actor is already protected by actor isolation.
actor MyNewDataManager {
// Simulates a blocking database fetch; returns synchronously for demo purposes.
func getDataFromDatabase() -> [String] {
return ["One", "Two", "Three", "Four", "Five", "Six"]
}
}
//@MainActor
// ObservableObject lets SwiftUI subscribe to @Published property changes.
class GlobalActorBootcampViewModel: ObservableObject {
// @MainActor on this single property means SwiftUI will always receive
// mutations on the main thread, even though the class itself is not @MainActor.
@MainActor @Published var dataArray: [String] = []
// @Published var dataArray1: [String] = []
// @Published var dataArray2: [String] = []
// @Published var dataArray3: [String] = []
// @Published var dataArray4: [String] = []
// Holds the global actor's shared actor instance for calling its methods.
let manager = MyFirstGlobalActor.shared
// nonisolated
// @MyFirstGlobalActor isolates this method to the custom global actor's serial queue.
// The compiler guarantees it won't run concurrently with other @MyFirstGlobalActor work.
@MyFirstGlobalActor func getData() {
// HEAVY COMPLEX METHODS
// Task{} starts a new child task. Because we're already on @MyFirstGlobalActor,
// the task inherits that isolation for the duration of the closure.
Task {
// `await` suspends until MyNewDataManager's actor is available to run the method.
let data = await manager.getDataFromDatabase()
// MainActor.run hops execution back to the main thread to update the UI safely.
await MainActor.run(body: {
self.dataArray = data
})
}
}
}
struct GlobalActorBootcamp: View {
// @StateObject ties the view model's lifetime to this view.
@StateObject private var viewModel = GlobalActorBootcampViewModel()
var body: some View {
ScrollView {
VStack {
// id: \.self works here because the strings are unique in this data set.
ForEach(viewModel.dataArray, id: \.self) {
Text($0)
.font(.headline)
}
}
}
// .task runs when the view appears and is automatically cancelled when it disappears.
.task {
// await required because getData() is isolated to @MyFirstGlobalActor.
await viewModel.getData()
}
}
}
struct GlobalActorBootcamp_Previews: PreviewProvider {
static var previews: some View {
GlobalActorBootcamp()
}
}Code Walkthrough
Declaring
@globalActor— The@globalActorattribute onMyFirstGlobalActorregisters it with the Swift compiler as a global isolation domain. Thestatic var sharedproperty must be anactortype; Swift uses its executor for all annotated work.The backing actor
MyNewDataManager— This is an ordinary actor.getDataFromDatabase()is safe to call from multiple tasks simultaneously because actor isolation serializes access. When Swift's executor runsawait manager.getDataFromDatabase(), it waits for any other in-flight call on this actor to finish first.Annotating a method with
@MyFirstGlobalActor— MarkinggetData()with the custom global actor means the compiler treats it exactly like@MainActor-annotated methods: callers outside the same actor domain mustawaitit, and the method body runs onMyFirstGlobalActor.shared's serial executor.Crossing back to
@MainActor— Afterawait manager.getDataFromDatabase()resolves, execution is still onMyFirstGlobalActor's executor.await MainActor.run { self.dataArray = data }is the explicit hop back to the main thread to assign the UI-facing property safely.@MainActor @Published var dataArray— Applying@MainActorto a single property rather than the whole class is fine-grained isolation. It guarantees this one property can only be mutated from the main actor, while the rest of the class remains unconstrained..taskmodifier — SwiftUI's.taskmodifier creates a structured task tied to the view's lifetime. When the view disappears, SwiftUI cancels the task automatically — no need to hold aTaskreference and call.cancel()manually.
Common Mistakes
Mistake: Putting slow or blocking work inside @MainActor and assuming a custom global actor is unnecessary.@MainActor serializes work on the main thread, which drives the UI render loop. Any computation that takes more than a few milliseconds will drop frames. Create a custom global actor whose executor runs off the main thread for heavy work, then hop back to @MainActor only for the final UI update.
Mistake: Creating a custom global actor when a single actor instance would suffice.
Global actors shine when the same isolation domain must be applied across many unrelated types throughout the app. If only one class needs isolation, a plain actor or a @MainActor-annotated class is simpler and less surprising to readers. Reach for @globalActor only when you genuinely need a named, app-wide shared domain.
Mistake: Annotating an entire type with a global actor when only one method needs isolation.
Marking a whole class with @MyFirstGlobalActor means every call into that class requires await, even methods that never touch the protected state. Prefer annotating only the specific properties or methods that truly need isolation, and use nonisolated to opt out the purely computational parts.
Key Takeaways
@globalActorcreates a singleton actor domain — all annotated code runs on the same serial executor, regardless of where in the codebase the annotation appears.@MainActoris itself a global actor; understanding custom global actors is the key to understanding why@MainActorbehaves the way it does.- Use
MainActor.run { }orawaitto explicitly cross actor boundaries; the compiler enforces that you cannot access another actor's isolated state without marking the boundary.
Last updated: June 27, 2026