top of page

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.


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.

bottom of page