Software Design

Swift Singleton Pattern: Why shared Is Really Called an Antipattern

When developing with Swift, you encounter singletons created with a single line, static let shared, very often.

4 min read
Cover image for Swift Singleton Pattern: Why shared Is Really Called an Antipattern

When developing with Swift, you encounter singletons created with static let shared very often.

When first building an iOS app, it is easy to cover everything—from the network manager and user session to cache management—with singletons.

Because it is convenient. You only need to call Manager.shared from anywhere.

But as the project grows, strange things start happening. Tests become impossible. You fix one screen, and a bug appears somewhere completely unrelated.

There are good reasons singletons are called an antipattern.

This article explains what the Swift Singleton Pattern really is, why overuse makes it an antipattern, and when it can still be used, from a practical perspective.

The conclusion is that the singleton itself is not bad; the problem is allowing its state to be changed globally from anywhere. Careless overuse of shared blocks testing, hides dependencies, and introduces concurrency problems.

Swift Singleton Pattern: What exactly is it?

A singleton is a design pattern that guarantees exactly one instance exists throughout the app.

In Swift, you can create one incredibly briefly. That is both its appeal and its trap.

final class NetworkManager {
    static let shared = NetworkManager()  // Only one in the app
    private init() {}                     // Block external creation
    func request(_ url: URL) { /* ... */ }
}

static let is initialized exactly once in a thread-safe manner by Swift. So instance creation itself is safe without a separate lock.

Using private init() to prevent creation from outside is also essential. Without it, you have just a global object, not a singleton.

So far, it looks wonderfully clean. The problem begins with the fact that it can be called from anywhere.


Why Is a shared Instance Called an Antipattern?

Let’s go through the three most common criticisms in the order I experienced them.

First, testing becomes hell.

If you call NetworkManager.shared directly in your code, there is no way to replace it with a mock object during testing.

Requests may go to the real server, or tests may share state, so changing their order changes the results.

Second, dependencies disappear from view.

If a function secretly uses UserSession.shared internally, its signature alone cannot tell you what the function needs.

This is especially dangerous when collaborating. You have to inspect all the code to see the dependency relationships.

Third, concurrency problems caused by global mutable state.

If a singleton contains properties and multiple threads read and write them simultaneously, a data race occurs.

Safe instance creation does not make mutations to the state inside it safe. Confusing these two can lead to crashes.

My code from the days when I covered everything with shared; looking at it now makes me shudder
My code from the days when I covered everything with shared; looking at it now makes me shudder

To summarize:

The real reason singletons are criticized is not that only one exists, but that they can be retrieved and changed from anywhere.


So, Should Singletons Always Be Avoided?

No. I still use them in specific situations.

Some things are naturally meant to exist only once. Apple also uses singletons in its standard libraries, such as UserDefaults.standard, FileManager.default, and URLSession.shared.

This guideline is useful:

  • The state rarely changes and access is mostly read-only → a singleton is fine
  • It truly must be unique across the app → consider a singleton
  • It must behave differently in tests → dependency injection is recommended

The key is: do not call shared directly inside the code; inject it from outside.

Use shared as the default and inject a fake for tests
Use shared as the default and inject a fake for tests

Even with the same singleton, changing the design this way greatly improves testability.

protocol Networking { func request(_ url: URL) }
extension NetworkManager: Networking {}

final class FeedViewModel {
    private let network: Networking
    init(network: Networking = .shared) {  // The default is a singleton; for tests, inject mock 
        self.network = network
    }
}

Using the singleton as the default is convenient in normal use, while injecting a fake object makes testing flexible.

You are not abandoning the singleton; you are only changing how it is accessed.


Three Rules I Follow in Practice

Finally, I’ll share the standards I set for myself in professional work.

  1. Do not create singletons whose mutable state is changed in multiple places. If state is needed, make its owner explicit.
  2. Do not call shared directly inside a function; inject it through the initializer or parameters.
  3. Make singletons with concurrent access actors, or protect them with a serial queue.

Following these three rules almost completely prevents the project from rotting because of singletons.

A singleton is not the wrong tool; it is simply so easy that it is prone to overuse.

Before scattering shared everywhere just because it is convenient, ask yourself once: “Can I test this later?” That single question will save me six months from now.