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
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
MainActor
can 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..."
}
Should I use Global actors for Signletons?
Yes, if:
- Your singleton deals with asynchronous operations or shared mutable states across different parts of your application.
- You are already working in an environment where concurrency is a concern.
No, if:
- Your singleton is simple with no need for asynchronous safety or if performance isn’t critically dependent on concurrency management.
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.
If you enjoyed this article, please feel free to follow me on my social media: