Greetings, traveler!
In Swift, Singletons are commonly employed to create globally accessible class instances where only one instance is desired, such as network managers or database controllers.
Note
By the way, you can read more about the Singleton design pattern, find out why so many developers say it is an antipattern, and learn how to fix all its issues.
But there is one problem—Singletons are not thread-safe by default. Let’s check an example.
class Singleton {
static let shared = Singleton()
private init() {}
var data: String = ""
}In Swift 6 mode, you will receive an error:
❌ Static property ‘shared’ is not concurrency-safe because non-‘Sendable’ type ‘Singleton’ may have shared mutable state
Why does this happen? In Swift 6, the concurrency model became stricter about shared mutable state. Classes are not Sendable by default, and our Singleton contains a mutable property. To avoid it, you can mark your class as @unchecked Sendable:
class Singleton: @unchecked Sendable {
static let shared = Singleton()
private init() {}
var data: String = ""
}But this message is not the only problem since this class is not thread-safe. So, marking it with @unchecked Sendable is not enough. Let’s fix it.
class Singleton: @unchecked Sendable {
static let shared = Singleton()
var data: String {
get {
queue.sync {
_data
}
}
set {
queue.async(flags: .barrier) {
self._data = newValue
}
}
}
private let queue = DispatchQueue(
label: "com.singleton.queue",
qos: .default,
attributes: .concurrent
)
private var _data: String = ""
private init() {}
}Now, we can be sure that access to data property is synchronized. But it looks a bit complicated now, isn’t it?
Global Actors
With Swift’s new concurrency model, Global Actors provides an alternative method for managing shared resources in a thread-safe manner.
Global actors in Swift are a concurrency feature introduced to manage shared mutable states across asynchronous operations. They are essentially actors that are accessible globally within your application. Swift provides a default global actor called MainActor, but you can also define your own custom global actors.
Global actors can bring several benefits:
- Concurrency Safety: Global actors ensure that all access to the singleton state is safely managed. Only one task can access the actor’s state at any time, which prevents race conditions in asynchronous contexts.
- Simplicity: They simplify code by automatically handling the dispatch to the correct execution context (like the main thread for
MainActor). This reduces boilerplate code for managing thread safety. - Performance: For operations that don’t involve UI updates, using a custom global actor instead of
MainActorcan prevent unnecessary work on the main thread, potentially improving app responsiveness.
So, if you are enough with the main thread only singleton, you can mark it like this:
@MainActor
class Singleton {
static let shared = Singleton()
var data: String = ""
private init() {}
}If not, you can create your own Global actor. Check out this example:
@globalActor
actor GlobalActor {
static let shared = GlobalActor()
}
@GlobalActor
class Singleton {
static let shared = Singleton()
var data: String = ""
private init() {}
func setData(_ string: String) {
data = string
}
}So, now you can call your singleton’s function like this:
await Singleton.shared.setData("Something new...")But what if you want to change the data property directly? In that case, you can’t write such a code since you will gain an error since we are outside of actor isolated context:
Singleton.shared.data = "Something new..."❌ Global actor ‘GlobalActor’-isolated property ‘data’ can not be mutated from the main actor
So, we can write something like this:
Task { @GlobalActor in
Singleton.shared.data = "Something new..."
}Or like this:
@GlobalActor
func doSomething() {
Singleton.shared.data = "Something new..."
}It’s also worth mentioning that global actors are not entirely free in terms of performance and API design. Because global actors isolate access to their state, every call that touches the actor requires an await, which may introduce additional suspension points and executor hops. This can affect the responsiveness of the application if the actor becomes a bottleneck or if it’s used too frequently from different concurrency domains. In addition, migrating existing code to a global actor can be challenging, as all actor-isolated properties and methods must respect the await boundary and cannot be accessed synchronously from non-isolated contexts.
Should I Use Global Actors for Singletons?
Choosing between a plain singleton, a synchronized singleton, or a global-actor-isolated singleton depends on how the object is used and how often it will be accessed across concurrency domains.
You should consider using a global actor if:
• Your singleton manages shared mutable state.
If the object owns data that can be read or written from multiple tasks, a global actor provides a safe and predictable isolation boundary without manual locking.
• The singleton performs asynchronous work.
If the methods run concurrently or interact with structured concurrency (e.g., async network calls, background processing), actor isolation dramatically reduces the chance of data races.
• You want the compiler to enforce safety.
Unlike a GCD-based approach, global actors let the compiler track isolation automatically, making refactoring and testing safer.
• State consistency is more important than raw performance.
If correctness is your primary goal, actor serialization is usually worth the cost.
You may not want to use a global actor if:
• Your singleton is essentially stateless or read-only.
Configuration objects, immutables, and value caches don’t need actor isolation. In many cases, such types can be made Sendable outright.
• The object is accessed very frequently and must remain non-suspending.
Actor isolation introduces await boundaries and potential executor hops. For latency-sensitive code paths, this overhead might be undesirable.
• You only need thread safety for a couple of properties.
A lightweight GCD barrier or a lock may be sufficient and cheaper than converting the entire API into async context.
• You need synchronous access from many parts of the app.
Actor-isolated properties cannot be mutated synchronously, which may force you to rewrite large parts of the codebase to use async access patterns.
In other words, global actors are an excellent fit when safety, clarity, and concurrency correctness are the priorities. If your singleton is simple, synchronous, or rarely shared across tasks, a more lightweight approach may work just as well.
In many cases, using an actor as the singleton itself is the simplest and most reliable solution. An actor already provides isolated mutable state, automatic thread safety, and enforcement of structured concurrency. If your singleton only needs to protect its own internal data and does not have to interoperate with Objective-C APIs or coordinate execution across multiple types, an actor-based singleton such as:
actor Singleton {
static let shared = Singleton()
...
}is more than enough.
A global actor, on the other hand, serves a different purpose. It defines a shared execution context that can isolate not just one object, but an entire group of classes, functions, and modules. This approach becomes useful when you need multiple types to run on the same actor executor, when you want to introduce concurrency safety without rewriting existing classes into actors, or when the type must remain a class (for example, because it inherits from NSObject or interacts with UIKit/AppKit). In these scenarios, a custom global actor allows you to keep the object-oriented design while still benefiting from Swift’s actor isolation model.
@globalActor
actor DataLayerActor {
static let shared = DataLayerActor()
}
@DataLayerActor
final class UserRepository { ... }
@DataLayerActor
final class CacheManager { ... }
@DataLayerActor
func loadInitialData() async { ... }Conclusion
Ultimately, the choice depends on your application’s complexity and your singleton’s specific requirements. Global actors can provide a clean, maintainable solution if you’re building for scale or dealing with complex state management. Otherwise, a simple static property might suffice.
