Using singletons in Swift 6
Published on: April 23, 2025Singletons generally speaking get a bad rep. People don’t like them, they cause issues, and generally speaking it’s just not great practice to rely on globally accessible mutable state in your apps. Instead, it’s more favorable to practice explicit dependency passing which makes your code more testable and reliable overall.
That said, sometimes you’ll have singletons. Or, more likely, you’ll want to have a a shared instance of something that you need in a handful of places in your app:
class AuthProvider {
static let shared = AuthProvider()
// ...
}
In Swift 6, this will lead to issues because Swift 6 doesn’t like non-Sendable types, and it also doesn’t like global mutable state.
In this post, you’ll learn about the reasons that Swift 6 will flag your singletons and shared instances as problematic, and we’ll see what you can do to satisfy the Swift 6 compiler. We’ll run through several different errors that you can get for your shared instances depending on how you’ve structured your code.
Static property 'shared' is not concurrency-safe because it is nonisolated global shared mutable state
We’ll start off with an error that you’ll get for any static property that’s mutable regardless of whether this property is used for a shared instance or not.
For example:
class AuthProvider {
// Static property 'shared' is not concurrency-safe because it
// is nonisolated global shared mutable state
static var shared = AuthProvider()
private init() {}
}
class GamePiece {
// Static property 'power' is not concurrency-safe because it
// is nonisolated global shared mutable state
static var power = 100
}
As you can see, both GamePiece
and AuthProvider
get the exact same error. They’re not concurrency-safe because they’re not isolated and they’re mutable. That means we might mutate this static let from multiple tasks and that would lead to data races (and crashes).
To resolve this error, we can take different approaches depending on the usage of our static var
. If we really need our static member to be mutable, we should make sure that we can safely mutate and that means we need to isolate our mutable state somehow.
Resolving the error when our static var needs to be mutable
We’ll start off by looking at our GamePiece
; it really needs power
to be mutable because we can upgrade its value throughout the imaginary game I have in mind.
Isolating GamePiece to the main actor
One approach is to isolate our GamePiece
or static var power
to the main actor:
// we can isolate our GamePiece to the main actor
@MainActor
class GamePiece {
static var power = 100
}
// or we isolate the static var to the main actor
class GamePiece {
@MainActor
static var power = 100
}
The first option makes sense when GamePiece is a class that’s designed to closely work with our UI layer. When we only ever work with GamePiece from the UI, it makes sense to isolate the entire object to the main actor. This simplifies our code and makes it so that we’re not going from the main actor’s isolation to some other isolation and back all the time.
Alternatively, if we don’t want or need the entire GamePiece
to be isolated to the main actor we can also choose to only isolate our static var
to the main actor. This means that we’re reading and writing power
from the main actor at all times, but we can work with other methods an properties on GamePiece
from other isolation contexts too. This approach generally leads to more concurrency in your app, and it will make your code more complex overall.
There’s a second option that we can reach for, but it’s one that you should only use if constraining your type to a global actor makes no sense.
It’s nonisolated(unsafe)
.
Allowing static var with nonisolated(unsafe)
Sometimes you’ll know that your code is safe. For example, you might know that power
is only accessed from a single task at a time, but you don’t want to encode this into the type by making the property main actor isolated. This makes sense because maybe you’re not accessing it from the main actor but you’re using a global dispatch queue or a detached task.
In these kinds of situations the only real correct solution would be to make GamePiece
an actor. But this is often non-trivial, introduces a lot of concurrency, and overall makes things more complex. When you’re working on a new codebase, the consequences wouldn’t be too bad and your code would be more “correct” overall.
In an existing app, you usually want to be very careful about introducing new actors. And if constraining to the main actor isn’t an option you might need an escape hatch that tells the compiler “I know you don’t like this, but it’s okay. Trust me.”. That escape hatch is nonisolated(unsafe)
:
class GamePiece {
nonisolated(unsafe) static var power = 100
}
When you mark a static var
as nonisolated(unsafe)
the compiler will no longer perform data-race protection checks for that property and you’re free to use it however you please.
When things are working well, that’s great. But it’s also risky; you’re now taking on the manual responsibility of prevent data races. And that’s a shame because Swift 6 aims to help us catch potential data races at compile time!
So use nonisolated(unsafe)
sparingly, mindfully, and try to get rid of it as soon as possible in favor of isolating your global mutable state to an actor.
Note that in Swift 6.1 you could make
GamePiece
an actor and the Swift compiler will allow you to havestatic var power = 100
without issues. This is a bug in the compiler and still counts as a potential data race. A fix has already been merged to Swift's main branch so I would expect that Swift 6.2 emits an appropriate error for having astatic var
on an actor.
Resolving the error for shared instances
When you’re working with a shared instance, you typically don’t need the static var
to be a var
at all. When that’s the case, you can actually resolve the original error quite easily:
class AuthProvider {
static let shared = AuthProvider()
private init() {}
}
Make the property a let
instead of a var
and Static property 'shared' is not concurrency-safe because it is nonisolated global shared mutable state
goes away.
A new error will appear though…
Static property 'shared' is not concurrency-safe because non-'Sendable' type 'AuthProvider' may have shared mutable state
Let’s dig into that error next.
Static property 'shared' is not concurrency-safe because non-'Sendable' type may have shared mutable state
While the new error sounds a lot like the one we had before, it’s quite different. The first error complained that the static var
itself wasn’t concurrency-safe, this new error isn’t complaining about the static let
itself. It’s complaining that we have a globally accessible instance of our type (AuthProvider
) which might not be safe to interact with from multiple tasks.
If multiple tasks attempt to read or mutate state on our instance of AuthProvider
, every task would interact with the exact same instance. So if AuthProvider
can’t handle that correctly, we’re in trouble.
The way to fix this, is to make AuthProvider
a Sendable
type. If you’re not sure that you fully understand Sendable just yet, make sure to read this post about Sendable so you’re caught up.
The short version of Sendable
is that a Sendable
type is a type that is safe to interact with from multiple isolation contexts.
Making AuthProvider Sendable
For reference types like our AuthProvider
being Sendable
would mean that:
AuthProvider
can’t have any mutable state- All members of
AuthProvider
must also be Sendable AuthProvider
must be afinal class
- We manually conform
AuthProvider
to theSendable
protocol
In the sample code, AuthProvider
didn’t have any state at all. So if we’d fix the error for our sample, I would be able to do the following:
final class AuthProvider: Sendable {
static let shared = AuthProvider()
private init() {}
}
By making AuthProvider
a Sendable
type, the compiler will allow us to have a shared instance without any issues because the compiler knows that AuthProvider
can safely be used from multiple isolation contexts.
But what if we add some mutable state to our AuthProvider
?
final class AuthProvider: Sendable {
static let shared = AuthProvider()
// Stored property 'currentToken' of
// 'Sendable'-conforming class 'AuthProvider' is mutable
private var currentToken: String?
private init() {}
}
The compiler does not allow our Sendable
type to have mutable state. It doesn’t matter that this state is private
, it’s simply not allowed.
Using nonisolated(unsafe) as an escape hatch again
If we have a shared instance with mutable state, we have several options available to us. We could remove the Sendable
conformance and make our static let
a nonisolated(unsafe)
property:
class AuthProvider {
nonisolated(unsafe) static let shared = AuthProvider()
private var currentToken: String?
private init() {}
}
This works but it’s probably the worst option we have because it doesn’t protect our mutable state from data races.
Leveraging a global actor to make AuthProvider Sendable
Alternatively, we could apply isolate our type to the main actor just like we did with our static var
:
// we can isolate our class
@MainActor
class AuthProvider {
static let shared = AuthProvider()
private var currentToken: String?
private init() {}
}
// or just the shared instance
class AuthProvider {
@MainActor
static let shared = AuthProvider()
private var currentToken: String?
private init() {}
}
The pros and cons of this solutions are the same as they were for the static var
. If we mostly use AuthProvider
from the main actor this is fine, but if we frequently need to work with AuthProvider
from other isolation contexts it becomes a bit of a pain.
Making AuthProvider an actor
My preferred solution is to either make AuthProvider
conform to Sendable
like I showed earlier, or to make AuthProvider
into an actor:
actor AuthProvider {
static let shared = AuthProvider()
private var currentToken: String?
private init() {}
}
Actors in Swift are always Sendable
which means that an actor can always be used as a static let
.
There’s one more escape hatch…
Let’s say we can’t make AuthProvider
an actor because we’re working with existing code and we’re not ready to pay the price of introducing loads of actor-related concurrency into our codebase.
Maybe you’ve had AuthProvider
in your project for a while and you’ve taken appropriate measures to ensure its concurrency-safe.
If that’s the case, @unchecked Sendable
can help you bridge the gap.
Using @unchecked Sendable as an escape hatch
Marking our class as @unchecked Sendable
can be done as follows:
final class AuthProvider: @unchecked Sendable {
static let shared = AuthProvider()
private var currentToken: String?
private init() {}
}
An escape hatch like this should be used carefully and should ideally be considered a temporary fix. The compiler won’t complain but you’re open to data-races that the compiler can help prevent altogether; it’s like a sendability force-unwrap.
In Summary
Swift 6 allows singletons, there’s no doubt about that. It does, however, impose pretty strict rules on how you define them, and Swift 6 requires you to make sure that your singletons and shared instances are safe to use from multiple tasks (isolation contexts) at the same time.
In this post, you’ve seen several ways to get rid of two shared instance related errors.
First, you saw how you can have static var
members in a way that’s concurrency-safe by leveraging actor isolation.
Next, you saw that static let
is another way to have a concurrency-safe static member as long as the type of your static let
is concurrency-safe. This is what you’ll typically use for your shared instances.
I hope this post has helped you grasp static members and Swift 6 a bit better, and that you’re now able to leverage actor isolation where needed to correctly have global state in your apps.