01 · INTRODUCTION
What Is a Global Actor in Swift?
The short answer
A global actor is a named, globally shared actor-isolation domain. It lets declarations that live in different types—and even different files—say, “this code and state must be accessed through the same actor.”
CODE EXAMPLE
@globalActor
actor LaunchDataActor {
static let shared = LaunchDataActor()
private init() {}
}
@LaunchDataActor
final class LaunchRepository {
// Isolated to LaunchDataActor
}
@LaunchDataActor
final class LaunchSearchIndex {
// Isolated to the same LaunchDataActor
}
LaunchRepository and LaunchSearchIndex are separate types, but their isolated code runs within the same domain. Swift therefore prevents them from being accessed concurrently in ways that would violate that isolation.
The central idea
An ordinary actor isolates one actor instance. A global actor can isolate declarations spread throughout the program by connecting them to one shared actor instance.
02 · TUTORIAL
From Instance Actors to a Global Actor
Each ordinary actor instance has its own domain
Every ordinary actor instance has its own isolation domain:
CODE EXAMPLE
actor LaunchCache {
private var launches: [Launch] = []
func replace(with launches: [Launch]) {
self.launches = launches
}
}
let europeanCache = LaunchCache()
let americanCache = LaunchCache()
europeanCache and americanCache do not share one actor. Each protects its own state, and the concurrency runtime may make progress on work for both actors independently.
That is usually exactly what we want. State that belongs to one object should normally be isolated by that object. But some program rules extend beyond a single instance. UI state must be updated through the main actor, for example, even when that state is spread across many screens, controllers and services.
A global actor describes this wider rule in Swift's type system.
MainActor is the global actor you already know
MainActor is the standard library's most familiar global actor. It represents the shared isolation domain used for main-executor work, including UI state in Apple applications.
CODE EXAMPLE
@MainActor
final class LaunchScreenModel {
var title = "Upcoming Launches"
var isLoading = false
}
@MainActor
func presentLaunchDetails() {
// UI work
}
The class and the function do not have to belong to the same object. Their @MainActor annotations place them in the same global isolation domain. Code already isolated to MainActor can use them synchronously. Code outside that domain must cross the boundary in an allowed way, commonly with await.
This is why @MainActor means more than “please dispatch this closure to the main queue.” It is part of the declaration's concurrency contract. The compiler can check callers, stored properties, protocol conformances and function values against that contract.
03 · TUTORIAL
How to Define a Custom Global Actor
A custom global actor is a type marked with @globalActor that supplies a stable shared actor instance:
CODE EXAMPLE
@globalActor
actor LaunchDataActor {
static let shared = LaunchDataActor()
private init() {}
}
The type name now also works as an attribute: @LaunchDataActor. Swift uses the shared actor to synchronize declarations carrying that attribute.
The private initializer is not what creates the isolation rule; @globalActor and shared do that. Making the initializer private is simply a useful design choice when nobody should create additional LaunchDataActor instances and mistake them for the globally shared domain.
Important Terminology
The global actor type gives the isolation domain its name. Its shared actor instance provides the mutually exclusive access used for declarations isolated to that domain.
04 · TUTORIAL
One Annotation Can Connect Different Types and Files
Imagine a launch application with a repository and a search index. Their definitions may live in different files, yet updating one without the other could leave the feature in an inconsistent state.
CODE EXAMPLE
struct Launch: Identifiable, Sendable {
let id: UUID
let name: String
let date: Date
}
@LaunchDataActor
final class LaunchRepository {
private var launchesByID: [Launch.ID: Launch] = [:]
func replace(with launches: [Launch]) {
launchesByID = Dictionary(
uniqueKeysWithValues: launches.map { ($0.id, $0) }
)
}
func snapshot() -> [Launch] {
launchesByID.values.sorted { $0.date < $1.date }
}
}
@LaunchDataActor
final class LaunchSearchIndex {
private var namesByID: [Launch.ID: String] = [:]
func rebuild(from launches: [Launch]) {
namesByID = Dictionary(
uniqueKeysWithValues: launches.map {
($0.id, $0.name.lowercased())
}
)
}
func ids(matching query: String) -> [Launch.ID] {
let query = query.lowercased()
return namesByID.compactMap { id, name in
name.contains(query) ? id : nil
}
}
func retain(ids: Set<Launch.ID>) {
namesByID = namesByID.filter { ids.contains($0.key) }
}
}
Every instance of both annotated classes is isolated to LaunchDataActor. This is deliberately different from defining each class as its own actor. Separate actor instances would provide separate isolation domains; the global-actor annotations connect both types to the same one.
05 · TUTORIAL
Code on the Same Global Actor Can Work Synchronously
A declaration already isolated to LaunchDataActor can call other declarations on that same global actor without await:
CODE EXAMPLE
@LaunchDataActor
func commit(
_ launches: [Launch],
to repository: LaunchRepository,
index: LaunchSearchIndex
) {
repository.replace(with: launches)
index.rebuild(from: repository.snapshot())
}
The function, repository and index all share the same isolation domain. There is no actor boundary between those calls.
This is one of the most useful properties of a global actor. A shared invariant can be maintained across several declarations without moving every operation and every piece of storage into one enormous actor type.
The isolation is still about exclusive access, not database transactions. If commit(_:to:index:) throws halfway through or explicitly suspends, Swift does not automatically roll back earlier mutations. Your domain still needs an appropriate error and consistency design.
06 · TUTORIAL
Crossing the Global-Actor Boundary Requires await
Code outside LaunchDataActor cannot synchronously enter its isolated declarations:
CODE EXAMPLE
func loadVisibleLaunches(
from repository: LaunchRepository
) async -> [Launch] {
let launches = await repository.snapshot()
return launches.filter { $0.date > .now }
}
The await marks a possible suspension while the task crosses to the global actor. It does not promise a new thread, and it does not say the operation is slow. It makes the isolation boundary visible.
Values crossing that boundary must also obey Swift's transfer rules. The example returns [Launch], whose element type is Sendable. Returning a mutable, non-Sendable reference would undermine the protection by allowing shared state to escape.
Applying a global actor to an entire type normally isolates its methods, properties and subscripts. A member that genuinely needs no protected state can opt out with nonisolated, but that decision should follow the data—not a desire to remove await.
07 · TUTORIAL
Global Actors and Instance Actors Solve Different Problems
Question
What owns the isolation?
Ordinary actor
Each actor instance
Global actor
One shared actor instance identified by a type
Question
What is protected?
Ordinary actor
The instance's stored state
Global actor
Annotated declarations across the program
Question
Do two instances share a domain?
Ordinary actor
No
Global actor
Yes, when their types use the same global actor
Question
Typical example
Ordinary actor
One cache, session or document
Global actor
UI isolation or a genuinely shared subsystem rule
Prefer an ordinary actor when the state naturally belongs to one instance. Reach for a custom global actor only when several declarations must participate in the same program-wide isolation rule.
08 · TUTORIAL
What a Global Actor Does Not Mean
It does not mean global state
A global actor is global in identity, not necessarily in storage. LaunchRepository still owns its repository state, and LaunchSearchIndex still owns its index. The annotation says how access is isolated; it does not place all values into a singleton dictionary.
It does not create a dedicated thread
A custom global actor does not reserve a permanent background thread. Swift schedules jobs through executors and the concurrency runtime. MainActor has special integration with the main executor, but a custom global actor should not be described as “the launch-data thread.”
It is not a serial dispatch queue
Both models may prevent two protected pieces of work from executing simultaneously, but actor scheduling is not a first-in, first-out queue contract. Priority, suspension and runtime scheduling can affect which job makes progress next.
It does not make every operation atomic
Actor isolation protects synchronous regions between suspension points. If an isolated async function reaches await, other work on the same actor may run before the original function resumes.
09 · TUTORIAL
Reentrancy and Task Inheritance Still Apply
Global actors follow the same important actor rules that the rest of this series has established.
CODE EXAMPLE
@LaunchDataActor
func refresh(
using api: LaunchAPI,
repository: LaunchRepository,
index: LaunchSearchIndex
) async throws {
let launches = try await api.fetchUpcoming()
// State may have changed while this function was suspended.
repository.replace(with: launches)
index.rebuild(from: repository.snapshot())
}
While fetchUpcoming() is suspended, the global actor is free to execute another job. When the function resumes, it regains isolation, but it must not assume that all actor-protected state is unchanged merely because the function began on the same actor.
A Task { } created from a global-actor-isolated context also inherits that actor isolation:
CODE EXAMPLE
@LaunchDataActor
func scheduleMaintenance(
for index: LaunchSearchIndex,
validIDs: Set<Launch.ID>
) {
Task {
// This task inherits LaunchDataActor isolation.
index.retain(ids: validIDs)
}
}
The new task is unstructured—it does not become a child task merely because it inherits isolation. The annotation does not change its lifetime, cancellation or result-management responsibilities. It also does not send the task to a background thread.
10 · TUTORIAL
When Should You Create a Custom Global Actor?
A custom global actor can be appropriate when a real invariant or external constraint spans multiple declarations. Examples include:
• a legacy subsystem whose related state must be accessed through one isolation domain;
• a process-wide resource represented by several cooperating types;
• a feature in which repository and index mutations must share one serialized isolation boundary; or
• an API surface that needs one named isolation rule across multiple modules.
The key phrase is one shared rule. A global actor is not merely a label for code that happens to discuss launches.
When not to create one
Do not create a custom global actor simply to:
• replace every serial dispatch queue;
• make code “run in the background”;
• group all types belonging to one feature;
• avoid deciding which object owns mutable state; or
• silence concurrency diagnostics by placing most of the application on one actor.
Over-isolation can serialize unrelated work and turn one global actor into a contention point. It also creates architectural coupling: declarations annotated with the actor now expose that isolation as part of their API contract.
For UI state, use MainActor. For independently owned mutable state, prefer ordinary actor instances. A custom global actor earns its place only when the shared domain itself explains something important about the system.
11 · TUTORIAL
A Complete Launch-Feature Boundary
The following application-shaped example keeps network work outside the custom global actor, then crosses the boundary once to commit a Sendable result:
CODE EXAMPLE
struct LaunchAPI: Sendable {
func fetchUpcoming() async throws -> [Launch] {
// Perform the request and decode Sendable values.
[]
}
}
struct LaunchSyncService: Sendable {
let api: LaunchAPI
let repository: LaunchRepository
let searchIndex: LaunchSearchIndex
func refresh() async throws {
let launches = try await api.fetchUpcoming()
await commit(
launches,
to: repository,
index: searchIndex
)
}
}
@MainActor
final class LaunchScreenModel {
private let repository: LaunchRepository
private(set) var launches: [Launch] = []
init(repository: LaunchRepository) {
self.repository = repository
}
func reloadFromStore() async {
launches = await repository.snapshot()
}
}
This small architecture contains three meaningful domains:
• LaunchAPI performs asynchronous boundary work and returns Sendable values.
• LaunchDataActor protects the repository and search-index invariant.
• MainActor protects state observed by the user interface.
The design does not put an entire feature on one actor. It gives each kind of state an explicit owner and makes crossings visible. That is the architectural value of Swift Concurrency: not a collection of keywords, but a vocabulary for describing which tasks perform work and which isolation domains protect state.
12 · TUTORIAL
Common Misunderstandings
“Global” means every type can access the state directly
No. The actor's identity is globally available as an isolation annotation. Normal access control and actor-isolation rules still apply.
Every instance gets a separate global actor
No. Declarations annotated with the same global actor use its same shared actor instance. That shared domain is the point.
await sends the work to the global actor's thread
No dedicated thread is implied. await exposes a possible suspension and actor crossing; the runtime and relevant executor schedule the work.
A custom global actor is safer than several ordinary actors
Neither design is universally safer. The correct choice follows ownership. Separate actors preserve independent isolation and potential concurrency. One global actor is useful when the same isolation rule genuinely spans those declarations.
Actor isolation removes logical races
It prevents unsynchronized access to isolated state, but an async operation may still resume with stale assumptions. Revalidate important state after suspension points.
13 · TUTORIAL
What to Remember
• A global actor is a named, globally shared actor-isolation domain.
• It can isolate functions, properties, types and extensions that live in different parts of a program.
• MainActor is Swift's standard global actor for main-executor work.
• A custom global actor provides one stable shared actor instance.
• Declarations on the same global actor can access one another synchronously.
• Crossing from elsewhere commonly requires await and safe boundary values.
• A global actor is not global storage, a dedicated thread or a FIFO dispatch queue.
• Reentrancy, task inheritance and unstructured-task lifetime rules still apply.
• Prefer ordinary actors for independently owned state and custom global actors for genuinely shared isolation rules.
14 · TUTORIAL
Frequently Asked Questions
Is MainActor a global actor?
Yes. MainActor is a global actor supplied by the Swift standard library. It provides the shared isolation domain associated with the main executor.
Can I create my own global actor?
Yes. Declare a type with @globalActor and provide a stable static shared actor instance. Use the resulting attribute only for a genuine cross-cutting isolation rule.
Can a global actor annotate an entire class?
Yes. The class's members are then generally isolated to that global actor. Individual members that do not need the protected domain may be declared nonisolated when their data makes that safe.
Does a custom global actor run on a background thread?
No. Actor isolation does not promise a particular background thread. It describes which executor-isolated domain must run the protected code.
Does a global actor guarantee execution order?
No. It provides mutually exclusive isolated execution, not a FIFO ordering guarantee. Suspension, priority and scheduling still matter.
Should every feature have its own global actor?
No. A feature name is not automatically an isolation domain. Start with clear state ownership and ordinary actors. Introduce a global actor only when several declarations truly share one isolation invariant.
15 · TUTORIAL
References
16 · TUTORIAL
Continue Learning
This article completes the planned conceptual path through Swift Concurrency. The next step is not another disconnected definition. It is to combine these building blocks in practical work: task ownership, cancellation, child tasks, Sendable values, actor isolation, reentrancy, MainActor and carefully chosen custom isolation domains.
A useful first project is the launch feature described here. Fetch upcoming launches, commit them through a protected data boundary, present them from a @MainActor screen model and make the screen's task lifetime control cancellation. At that point, the codebase becomes easier to describe as a collection of features, tasks and explicitly owned state—not merely a collection of types and queues.
17 · TUTORIAL
Download the Xcode Playgrounds
The companion playgrounds for this article should include a guided exploration and a separate challenge set. They will let readers compare instance actors with a custom global actor, observe compiler-enforced crossings, explore reentrancy and refactor an over-isolated launch feature into clearer ownership domains.
