Swift
singleton pattern
dispatch_once
software design
iOS development

Using a dispatch_once singleton model in Swift

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

In modern Swift, you do not implement a singleton with dispatch_once. The idiomatic replacement is a static let shared property, which is lazily initialized and thread-safe by language guarantee.

That means the real question today is not "how do I use dispatch_once in Swift?" but "how do I express a singleton correctly in Swift without old Objective-C patterns?"

The Modern Swift Singleton

The standard pattern is:

swift
1final class SettingsStore {
2    static let shared = SettingsStore()
3
4    private init() {
5    }
6
7    func load() {
8        print("loading settings")
9    }
10}
11
12SettingsStore.shared.load()

This gives you:

  • one shared instance
  • lazy initialization on first use
  • thread-safe initialization
  • a private initializer that prevents accidental extra instances

That covers the same practical need that dispatch_once used to cover in older Objective-C code.

Why dispatch_once Went Away

Older Cocoa code often looked like this in Objective-C:

objective-c
1static dispatch_once_t onceToken;
2dispatch_once(&onceToken, ^{
3    shared = [[MyManager alloc] init];
4});

Swift does not need that pattern for singleton initialization. Static stored properties already provide the same one-time, thread-safe initialization behavior. Using dispatch_once manually in Swift is not only unnecessary, it also makes the code look dated and less idiomatic.

Keep The Initializer Private

The private initializer is what turns a shared instance into an actual singleton rather than just a convenient global object.

swift
1final class Logger {
2    static let shared = Logger()
3
4    private init() {
5    }
6}

Without the private initializer, any other part of the app could still create more instances and defeat the point of the pattern.

Singleton Does Not Mean Global Free-For-All

A singleton is useful for resources that are genuinely application-wide and logically unique, such as:

  • analytics dispatch
  • shared configuration cache
  • process-wide logging
  • app-level coordination services

It is less appropriate for things that should be dependency-injected, scoped per feature, or mocked heavily in tests. The fact that Swift makes singletons easy does not mean every service should become one.

Concurrency Considerations

The singleton instance creation is thread-safe, but the singleton's mutable state is not automatically safe just because the instance itself is shared correctly. If the singleton holds mutable data accessed from multiple tasks or threads, you still need synchronization.

For concurrency-heavy code, an actor can be a better shared singleton-like model:

swift
1actor RequestCache {
2    static let shared = RequestCache()
3
4    private var values: [String: String] = [:]
5
6    func set(_ value: String, for key: String) {
7        values[key] = value
8    }
9
10    func get(_ key: String) -> String? {
11        values[key]
12    }
13}

This preserves the single shared instance idea while giving the mutable state a safer concurrency model.

Testing And Design Tradeoffs

Singletons can make testing harder because they hide dependencies behind global access. A class that silently reads Logger.shared everywhere is harder to substitute in tests than a class that receives a logger through its initializer.

That does not make singletons forbidden. It means they should be used intentionally. If a dependency is logically global and process-wide, a singleton may be fine. If it is merely convenient, dependency injection is often cleaner.

Common Pitfalls

One common mistake is searching for a literal dispatch_once replacement when Swift's static let already solves the initialization problem. Another is assuming thread-safe initialization means thread-safe mutation of all singleton state. Developers also often create singletons for services that would be easier to test and reason about if they were injected explicitly. Finally, if the class is not marked final and the initializer is not private, the singleton model becomes easier to misuse.

Summary

  • In modern Swift, use static let shared instead of dispatch_once.
  • A private initializer is what makes the shared instance a real singleton.
  • Static stored properties are lazily initialized and thread-safe by design.
  • Singleton initialization safety does not automatically make mutable state safe.
  • Use the pattern sparingly and prefer dependency injection when the dependency is not truly global.

Course illustration
Course illustration

All Rights Reserved.