Singletons usually talking get a nasty rep. Folks don’t like them, they trigger points, and customarily talking it’s simply not nice observe to depend on globally accessible mutable state in your apps. As a substitute, it’s extra favorable to observe specific dependency passing which makes your code extra testable and dependable general.
That mentioned, typically you’ll have singletons. Or, extra seemingly, you’ll need to have a a shared occasion of one thing that you simply want in a handful of locations in your app:
class AuthProvider {
static let shared = AuthProvider()
// ...
}
In Swift 6, this can result in points as a result of Swift 6 doesn’t like non-Sendable varieties, and it additionally doesn’t like world mutable state.
On this publish, you’ll be taught concerning the causes that Swift 6 will flag your singletons and shared cases as problematic, and we’ll see what you are able to do to fulfill the Swift 6 compiler. We’ll run via a number of completely different errors that you could get to your shared cases relying on the way you’ve structured your code.
Static property ‘shared’ will not be concurrency-safe as a result of it’s nonisolated world shared mutable state
We’ll begin off with an error that you simply’ll get for any static property that’s mutable no matter whether or not this property is used for a shared occasion or not.
For instance:
class AuthProvider {
// Static property 'shared' will not be concurrency-safe as a result of it
// is nonisolated world shared mutable state
static var shared = AuthProvider()
personal init() {}
}
class GamePiece {
// Static property 'energy' will not be concurrency-safe as a result of it
// is nonisolated world shared mutable state
static var energy = 100
}
As you may see, each GamePiece
and AuthProvider
get the very same error. They’re not concurrency-safe as a result of they’re not remoted and so they’re mutable. Which means we’d mutate this static let from a number of duties and that will result in information races (and crashes).
To resolve this error, we are able to take completely different approaches relying on the utilization of our static var
. If we actually want our static member to be mutable, we should always ensure that we are able to safely mutate and meaning we have to isolate our mutable state someway.
Resolving the error when our static var must be mutable
We’ll begin off by our GamePiece
; it actually wants energy
to be mutable as a result of we are able to improve its worth all through the imaginary recreation I keep in mind.
Isolating GamePiece to the principle actor
One strategy is to isolate our GamePiece
or static var energy
to the principle actor:
// we are able to isolate our GamePiece to the principle actor
@MainActor
class GamePiece {
static var energy = 100
}
// or we isolate the static var to the principle actor
class GamePiece {
@MainActor
static var energy = 100
}
The primary choice is smart when GamePiece is a category that’s designed to intently work with our UI layer. After we solely ever work with GamePiece from the UI, it is smart to isolate the whole object to the principle actor. This simplifies our code and makes it in order that we’re not going from the principle actor’s isolation to another isolation and again on a regular basis.
Alternatively, if we don’t need or want the whole GamePiece
to be remoted to the principle actor we are able to additionally select to solely isolate our static var
to the principle actor. Which means that we’re studying and writing energy
from the principle actor always, however we are able to work with different strategies an properties on GamePiece
from different isolation contexts too. This strategy usually results in extra concurrency in your app, and it’ll make your code extra complicated general.
Making GamePiece an actor
A second strategy to fixing our error is to make GamePiece
into an actor:
actor GamePiece {
static var energy = 100
}
After we do that, all entry to energy
turns into remoted to GamePiece
. Which means that we are able to now safely work together with energy
from completely different isolation contexts however it additionally implies that we’ll must await any methodology calls and different property accesses that we carry out from exterior of the GamePiece
actor:
actor GamePiece {
static var energy = 100
func hearth() {
// ...
}
}
// it is a nonisolated async perform
func interactWithGamePiece() async {
// that is nice
GamePiece.energy += 1
let myGamePiece = GamePiece()
// we should await hearth regardless that it is not async
await myGamePiece.hearth()
}
Discover how we have to await
the decision to hearth
. The reason being that we’re calling an actor-isolated methodology (hearth
) from a nonisolated context (interactWithGamePiece
). Which means that by making GamePiece
an actor we’re introducing (loads of) concurrency in our codebase. This may not be fascinating.
At this level, now we have explored two choices:
- Constrain
energy
to the principle actor (both via its containing sort, or by constraining the static var solely) - Make
GamePiece
an actor (and introduce a great deal of concurrency)
There’s a 3rd choice that we are able to attain for, however it’s one which you must solely use if the 2 choices above make completely no sense to your app.
It’s nonisolated(unsafe)
.
Permitting static var with nonisolated(unsafe)
Generally you’ll know that your code is secure. For instance, you may know that energy
is simply accessed from a single process at a time, however you don’t need to encode this into the kind by making the property major actor remoted. This is smart as a result of perhaps you’re not accessing it from the principle actor however you’re utilizing a worldwide dispatch queue or a indifferent process.
In these sorts of conditions the one actual right resolution could be to make GamePiece
an actor. However that is usually non-trivial, introduces loads of concurrency, and general makes issues extra complicated. Whenever you’re engaged on a brand new codebase, the implications wouldn’t be too dangerous and your code could be extra “right” general.
In an current app, you often need to be very cautious about introducing new actors. And if constraining to the principle actor isn’t an choice you may want an escape hatch that tells the compiler “I do know you don’t like this, however it’s okay. Belief me.”. That escape hatch is nonisolated(unsafe)
:
class GamePiece {
nonisolated(unsafe) static var energy = 100
}
Whenever you mark a static var
as nonisolated(unsafe)
the compiler will now not carry out data-race safety checks for that property and also you’re free to make use of it nevertheless you please.
When issues are working nicely, that’s nice. But it surely’s additionally dangerous; you’re now taking up the guide accountability of forestall information races. And that’s a disgrace as a result of Swift 6 goals to assist us catch potential information races at compile time!
So use nonisolated(unsafe)
sparingly, mindfully, and attempt to eliminate it as quickly as attainable in favor of isolating your world mutable state to an actor.
Resolving the error for shared cases
Whenever you’re working with a shared occasion, you sometimes don’t want the static var
to be a var
in any respect. When that’s the case, you may really resolve the unique error fairly simply:
class AuthProvider {
static let shared = AuthProvider()
personal init() {}
}
Make the property a let
as an alternative of a var
and Static property 'shared' will not be concurrency-safe as a result of it's nonisolated world shared mutable state
goes away.
A brand new error will seem although…
Static property ‘shared’ will not be concurrency-safe as a result of non-‘Sendable’ sort ‘AuthProvider’ could have shared mutable state
Let’s dig into that error subsequent.
Static property ‘shared’ will not be concurrency-safe as a result of non-‘Sendable’ sort could have shared mutable state
Whereas the brand new error sounds rather a lot just like the one we had earlier than, it’s fairly completely different. The primary error complained that the static var
itself wasn’t concurrency-safe, this new error isn’t complaining concerning the static let
itself. It’s complaining that now we have a globally accessible occasion of our sort (AuthProvider
) which could not be secure to work together with from a number of duties.
If a number of duties try and learn or mutate state on our occasion of AuthProvider
, each process would work together with the very same occasion. So if AuthProvider
can’t deal with that accurately, we’re in bother.
The way in which to repair this, is to make AuthProvider
a Sendable
sort. In case you’re unsure that you simply totally perceive Sendable simply but, ensure to learn this publish about Sendable so that you’re caught up.
The quick model of Sendable
is {that a} Sendable
sort is a sort that’s secure to work together with from a number of isolation contexts.
Making AuthProvider Sendable
For reference varieties like our AuthProvider
being Sendable
would imply that:
AuthProvider
can’t have any mutable state- All members of
AuthProvider
should even be Sendable AuthProvider
should be aultimate class
- We manually conform
AuthProvider
to theSendable
protocol
Within the pattern code, AuthProvider
didn’t have any state in any respect. So if we’d repair the error for our pattern, I might be capable to do the next:
ultimate class AuthProvider: Sendable {
static let shared = AuthProvider()
personal init() {}
}
By making AuthProvider
a Sendable
sort, the compiler will enable us to have a shared occasion with none points as a result of the compiler is aware of that AuthProvider
can safely be used from a number of isolation contexts.
However what if we add some mutable state to our AuthProvider
?
ultimate class AuthProvider: Sendable {
static let shared = AuthProvider()
// Saved property 'currentToken' of
// 'Sendable'-conforming class 'AuthProvider' is mutable
personal var currentToken: String?
personal init() {}
}
The compiler doesn’t enable our Sendable
sort to have mutable state. It doesn’t matter that this state is personal
, it’s merely not allowed.
Utilizing nonisolated(unsafe) as an escape hatch once more
If now we have a shared occasion with mutable state, now we have a number of choices accessible to us. We may take away the Sendable
conformance and make our static let
a nonisolated(unsafe)
property:
class AuthProvider {
nonisolated(unsafe) static let shared = AuthProvider()
personal var currentToken: String?
personal init() {}
}
This works however it’s most likely the worst choice now we have as a result of it doesn’t shield our mutable state from information races.
Leveraging a worldwide actor to make AuthProvider Sendable
Alternatively, we may apply isolate our sort to the principle actor identical to we did with our static var
:
// we are able to isolate our class
@MainActor
class AuthProvider {
static let shared = AuthProvider()
personal var currentToken: String?
personal init() {}
}
// or simply the shared occasion
class AuthProvider {
@MainActor
static let shared = AuthProvider()
personal var currentToken: String?
personal init() {}
}
The professionals and cons of this options are the identical as they have been for the static var
. If we principally use AuthProvider
from the principle actor that is nice, but when we often must work with AuthProvider
from different isolation contexts it turns into a little bit of a ache.
Making AuthProvider an actor
My most popular resolution is to both make AuthProvider
conform to Sendable
like I confirmed earlier, or to make AuthProvider
into an actor:
actor AuthProvider {
static let shared = AuthProvider()
personal var currentToken: String?
personal init() {}
}
Actors in Swift are all the time Sendable
which implies that an actor can all the time be used as a static let
.
There’s yet one more escape hatch…
Let’s say we are able to’t make AuthProvider
an actor as a result of we’re working with current code and we’re not able to pay the value of introducing a great deal of actor-related concurrency into our codebase.
Possibly you’ve had AuthProvider
in your challenge for some time and also you’ve taken applicable measures to make sure its concurrency-safe.
If that’s the case, @unchecked Sendable
may help you bridge the hole.
Utilizing @unchecked Sendable as an escape hatch
Marking our class as @unchecked Sendable
will be performed as follows:
ultimate class AuthProvider: @unchecked Sendable {
static let shared = AuthProvider()
personal var currentToken: String?
personal init() {}
}
An escape hatch like this needs to be used fastidiously and will ideally be thought of a brief repair. The compiler received’t complain however you’re open to data-races that the compiler may help forestall altogether; it’s like a sendability force-unwrap.
In Abstract
Swift 6 permits singletons, there’s little doubt about that. It does, nevertheless, impose fairly strict guidelines on the way you outline them, and Swift 6 requires you to ensure that your singletons and shared cases are secure to make use of from a number of duties (isolation contexts) on the similar time.
On this publish, you’ve seen a number of methods to eliminate two shared occasion associated errors.
First, you noticed how one can have static var
members in a method that’s concurrency-safe by leveraging actor isolation.
Subsequent, you noticed that static let
is one other approach to have a concurrency-safe static member so long as the kind of your static let
is concurrency-safe. That is what you’ll sometimes use to your shared cases.
I hope this publish has helped you grasp static members and Swift 6 a bit higher, and that you simply’re now in a position to leverage actor isolation the place wanted to accurately have world state in your apps.