So far, we’ve been mostly discussing regular actors. There’s another type of actor: a Global Actor. The name describes it a bit already—it’s an actor that’s globally accessible. You can see it as a singleton, kind of, but using it works a little different.

What is a Global Actor?

We’ve seen how actors help isolate state inside a specific type — like the BankAccount or Counter actors. But what if you’ve got shared state that lives outside of a single actor? Like a global variable or a static property? That’s where global actors come in.

A global actor is exactly what it sounds like: an actor that you can apply globally. It brings the same kind of actor isolation — safe, serialized access to data — but instead of being tied to a single instance, it’s tied to something broader: like a function, a property, or even an entire type.

Here’s an example you’ve probably seen or used already:

@MainActor
func updateUI() {
    // Safely runs on the main thread
}

By marking this function with @MainActor, you’re basically saying:

“This should always run on the main thread.”

Behind the scenes, @MainActor is a global actor. It ensures that everything it touches is run on the same actor executor— in this case, the executor tied to the main thread. That makes it perfect for UI updates, which must happen on the main thread in apps.

You can also use global actors on properties:

@MainActor
var titles: [String] = []

Whether static or not. Finally, you can use them on entire types:

@MainActor
final class ContentViewModel {

    var titles: [String] = []

    /// ...
}

Which, depending on your case, might make more sense. In this case, all access to ContentViewModel will need to happen on the @MainActor executor. In other words, access is isolated to the global @MainActor.

But you’re not limited to the main thread. You can also create custom global actors to group and isolate access to global or static state — making your app more robust in a concurrent world.

Why do global actors matter?

As we’ve learned, global state is risky in concurrent programs. Without proper isolation, you can easily introduce data races as shared mutable state might be accessed globally from different threads.

Global actors resolve these cases by allowing you to apply actor isolation to global variables, static properties, functions that access shared state, and even entire types such as view models or service layers.

It’s like putting a “Serialized access only” sign on parts of your codebase that needs to be protected against data races, no matter where they live.

How to use a custom Global Actor?

We’ve seen examples of the standard @MainActor quite a few times now, but what if you want to define a custom global actor?

Imagine having an app that does several things, including image processing. You don’t want multiple images to be processed at the same time. You also want anything that has to do with image processing be running synchronized.

In this case, we can define a custom Global Actor named ImageProcessing:

@globalActor
actor ImageProcessing {
    static let shared = ImageProcessing()
}

The @globalActor attribute makes the ImageProcessing actor a globally accessible actor. It also requires the type to conform to the GlobalActor protocol, for which defining the static shared property will be enough.

Once defined, we can start using it just like we can with @MainActor. For example, we could use it on an image cache:

@ImageProcessing
final class ImageCache {

    var images: [URL: Data] = [:]

    /// Image cache logic...
}

Or maybe we have some kind of image filtering method:

@ImageProcessing
func applyFilter(_ inputImage: UIImage) -> UIImage {
    /// Apply filter to an image ...
}

Each of those will now execute on the ImageProcessing actor isolation domain, allowing you to centralize work related to image processing.

Why it’s better to use a private initializer

The above example uses the @globalActor attribute directly attached to the actor instance itself. This has the downside that we’re not preventing anyone from using the ImageProcessing actor directly:

/// This is still possible:
ImageProcessing()

Doing so would create a new actor with a new executor underneath. This is not what we aimed to achieve—we want all image processing to be running on the same executor.

Therefore, I recommend making the initializer of your global actors private:

@globalActor actor ImageProcessing {
    public static let shared = ImageProcessing()

    private init() { }
}

The actor can’t be constructed directly anymore, preventing duplicate instances from being created.

Summary

Global actors allow us to use actor isolation globally. We can benefit from compile-time synchronized access via the actor’s executor across our entire codebase. Global actors are flexible in that they allow us to enforce isolation on entire types, global variables, methods, and more.

In the next lesson, we’re going to look into the most used global actor: @MainActor.