Singleton in Swift

Roopesh Tripathi
4 min readOct 17, 2024

--

Explained thread safety and Mocking Singleton for Unit Test

the Singleton design pattern ensures that a class has only one instance and provides a global access point to it. It is commonly used when only one instance of a class should exist, such as for managing shared resources like configuration settings, logging, or network connections.

Here’s how to implement the Singleton pattern in Swift:

Implementation of a Singleton in Swift:

class Singleton {
// A static constant to hold the singleton instance
static let shared = Singleton()

// A private initializer to prevent creating multiple instances
private init() {
// Initialization code, if needed
print("Singleton Initialized!")
}

// Example method within the Singleton class
func doSomething() {
print("Doing something with the Singleton!")
}
}

Key Points:

  1. static let shared: This creates a single instance of the Singleton class, accessible globally through Singleton.shared. The let keyword ensures the instance is immutable.
  2. Private initializer (private init()): By making the initializer private, you prevent external code from creating new instances of the class.
  3. Global access: The singleton instance can be accessed globally via Singleton.shared.

Example Usage:

Singleton.shared.doSomething()
// Output: Doing something with the Singleton!

Benefits:

  • Global access: You can access the singleton instance from anywhere in the app.
  • Controlled instantiation: The class ensures only one instance is created.

Common Use Cases:

  • Shared configurations (e.g., app settings).
  • Centralized logging.
  • Networking managers.
  • Database access layers.

The Singleton pattern in Swift is simple, and thread-safe by design (due to static let), and a widely used approach for shared, global objects.

Now let's prove that creating a singleton this way will not cause a thread safety issue

Here’s a breakdown of why it is thread-safe:

Swift’s static let and Thread Safety

  • When you use static let, Swift uses lazy initialization internally, meaning the instance is created the first time it's accessed.
  • The creation of the instance is done in a thread-safe manner using GCD (Grand Central Dispatch), ensuring that no race conditions occur, even if multiple threads try to access the singleton simultaneously.

Let’s take a deeper look:

class Singleton {
static let shared = Singleton() // Thread-safe lazy initialization

private init() {
print("Singleton initialized")
}

func doSomething() {
print("Singleton doing something")
}
}

Multithreaded Test Example

To test if the Singleton works in a multithreaded environment, we can simulate multiple threads accessing it:

import Foundation

// A function to simulate work from different threads
func testMultithreadedSingleton() {
let queue = DispatchQueue.global(qos: .userInitiated)

// Execute on 10 different threads concurrently
for _ in 1...10 {
queue.async {
// Access the shared instance in multiple threads
let instance = Singleton.shared
instance.doSomething()
}
}
}

// Run the test
testMultithreadedSingleton()

Expected Output:

  • The init method will only be called once, regardless of how many threads access the Singleton.shared concurrently.
  • All threads will see the same singleton instance.

Sample output:

Singleton initialized
Singleton doing something
Singleton doing something
Singleton doing something
Singleton doing something
...

Explanation:

  • Thread-safety: The static let declaration ensures that the Singleton instance is initialized once and only once no matter how many threads try to access it simultaneously.
  • Lazy initialization: The first access to Singleton.shared triggers the initialization, but Swift guarantees that this happens exactly once.

Conclusion:

The static let in Swift ensures that the Singleton instance is created in a thread-safe way, making it fully reliable for use in multithreaded environments without any additional synchronization mechanisms (like locks). The underlying GCD guarantees that the instance is initialized only once and prevents race conditions.

Mocking a singleton for unit testing

For Mocking singleton we have change static let to static var

will have a greater impact on the mutability of the object in a multithreaded environment.

let's solve this problem with a Dispatch barrier

class Singleton {
private static var privateShared: Singleton = Singleton() // Private storage for the singleton instance
private static let queue = DispatchQueue(label: "com.singleton.threadsafe", attributes: .concurrent) // Concurrent queue with a barrier

// Public accessor for the singleton instance with thread-safe access
static var shared: Singleton {
get {
return queue.sync {
return privateShared
}
}
set {
queue.async(flags: .barrier) {
privateShared = newValue
}
}
}

private init() {
print("Singleton Initialized!")
}

func doSomething() -> String {
return "Real implementation"
}
}

Explanation:

  1. Private privateShared: This is the actual storage of the singleton instance, which we control using the queue.
  2. Concurrent Dispatch Queue: We use a concurrent dispatch queue for thread-safe reads and writes.
  3. Barrier Flag (.barrier): The .barrier flag is used in async(flags: .barrier) to ensure that writes to privateShared are exclusive (i.e., no reads or writes happen concurrently when modifying the singleton).
  4. Sync Access: For reading the singleton, we use queue.sync to ensure thread-safe access.

Mocking in Unit Tests:

Since shared is a var, we can still override the singleton for testing purposes, and the barrier ensures safe access in multithreaded environments.

Example Test with Mock:

class MockSingleton: Singleton {
override func doSomething() -> String {
return "Mock implementation"
}
}

class SingletonTests: XCTestCase {

override func setUp() {
super.setUp()
// Inject mock singleton safely
Singleton.shared = MockSingleton()
}

override func tearDown() {
super.tearDown()
// Reset the singleton safely after testing
Singleton.shared = Singleton()
}

func testSingleton() {
let result = Singleton.shared.doSomething()
XCTAssertEqual(result, "Mock implementation")
}
}

Summary:

  • Thread Safety: You are correct that using static var directly without synchronization could cause race conditions in multithreaded environments.
  • Solution: Use a concurrent DispatchQueue with a barrier for writes to ensure thread-safe access to the singleton instance.
  • Mocking: You can still mock the singleton safely during unit tests by overriding the instance using the thread-safe accessor (static var shared).

--

--

Roopesh Tripathi
Roopesh Tripathi

Written by Roopesh Tripathi

👋 Hello, I'm Roopesh Tripathi! 📱 Mobile Developer | iOS Enthusiast | Swift Advocate 💻 Passionate about crafting elegant and efficient mobile apps.

No responses yet