13.3 C
Canberra
Wednesday, April 23, 2025

Utilizing singletons in Swift 6 – Donny Wals


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 a ultimate class
  • We manually conform AuthProvider to the Sendable 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.

Related Articles

LEAVE A REPLY

Please enter your comment!
Please enter your name here

[td_block_social_counter facebook="tagdiv" twitter="tagdivofficial" youtube="tagdiv" style="style8 td-social-boxed td-social-font-icons" tdc_css="eyJhbGwiOnsibWFyZ2luLWJvdHRvbSI6IjM4IiwiZGlzcGxheSI6IiJ9LCJwb3J0cmFpdCI6eyJtYXJnaW4tYm90dG9tIjoiMzAiLCJkaXNwbGF5IjoiIn0sInBvcnRyYWl0X21heF93aWR0aCI6MTAxOCwicG9ydHJhaXRfbWluX3dpZHRoIjo3Njh9" custom_title="Stay Connected" block_template_id="td_block_template_8" f_header_font_family="712" f_header_font_transform="uppercase" f_header_font_weight="500" f_header_font_size="17" border_color="#dd3333"]
- Advertisement -spot_img

Latest Articles