Using singletons in Swift 6

Published on: April 23, 2025

Singletons 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 have static 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 a static 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 a final class
  • We manually conform AuthProvider to the Sendable 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.

Subscribe to my newsletter