Singleton Design Pattern in Swift


Greetings, traveler!

Today, I want to discuss the Singleton design pattern as part of our Design Pattern Series. Singleton is one of the most famous and controversial design patterns. It is very popular and even used a lot by Apple, but it is also considered an anti-pattern. But why?

First, let me explain Singleton. It is a Creational Design Pattern that ensures only a single instance of a class exists throughout an application. Let’s create an example.

final class Singletone {
    
    static let shared: Singletone = .init()
    
    var name = "Name"
    var data = ["a", "b", "c"]
    
    private init() {}
    
    func change(_ name: String) {
        self.name = name
    }
    
    func change(_ data: [String]) {
        self.data = data
    }
    
}

Singleton’s popularity stems from its convenience and simplicity. It’s like a global variable — you don’t have to think too hard. At first glance, it seems like a lifesaver, promising to make your coding journey a breeze. However, Singleton is not without its challenges.

Problems of Singleton

Let’s explore some of the common issues that can arise when using this design pattern. 

  1. Since Singleton is a guy who is always alive in the memory of your application, it will be hard to test your app because you can’t be sure that Singleton wasn’t modified already.
  2. It is not easy to control the changes of an object that is accessible throughout the application, so sometimes, you can get an unexpected result.
  3. You can’t be sure if any code can be relied on for a singleton class, and maintaining such code can sometimes become challenging.
  4. Not every object that uses Singleton needs to use all its functionality. So, there is no reason for this functionality to be accessible to this object. 
  5. It can be tricky to ensure that Singleton’s functionality is Thread safe.

Should we really avoid using the Singleton pattern? In my opinion, we should use it with an understanding of what we are doing, such as when using inheritance or protocols. Think twice about why you need a Singleton, and then decide whether to use it or not. 

Anyway, there are some tricks that can make the usage of Singleton a bit cleaner.

Protocols and Dependency Injection

We can use protocols with our Singleton so that no one will interact with it directly, but only through protocols. We can inject our Singleton through the constructor. This will open access to only the functionality we need right now. It will also make the code much easier to understand: there won’t be any hidden Singleton-related surprises inside the object codebase. Additionally, it will help separate the Singleton’s functionality into different services.

protocol NameManagerProtocol {
    func change(_ name: String)
}

protocol DataManagerProtocol {
    func change(_ data: [String])
}

final class Singletone: NameManagerProtocol, DataManagerProtocol {
    
    static let shared: Singletone = .init()
    
    var name = "Name"
    var data = ["a", "b", "c"]
    
    private init() {}
    
    func change(_ name: String) {
        self.name = name
    }
    
    func change(_ data: [String]) {
        self.data = data
    }
    
}

final class ViewModel {
    
    private let nameManager: NameManagerProtocol
    private var name: String
    
    init(nameManager: NameManagerProtocol) {
        self.nameManager = nameManager
        self.name = nameManager.name
    }
    
    func update(_ name: String) {
        self.name = name
        nameManager.change(name)
    }
    
}

let viewModel = ViewModel(nameManager: Singletone.shared)

Thread safe Singleton

When you write a new value to a Singleton’s property, you may face a situation where you can’t guarantee the thread safety of access to this property. This can lead to a potential crash of your app. To avoid this, you can add a simple workaround using GCD.

final class Singletone: NameManagerProtocol {
    
    static let shared: Singletone = .init()
    
    var name: String {
        get {
            queue.sync {
                _name
            }
        }
        set {
            queue.async(flags: .barrier) {
                self._name = newValue
            }
        }
    }
    
    private let queue = DispatchQueue(
        label: "com.singletion.queue",
        qos: .default,
        attributes: .concurrent
    )
    
    private var _name: String = "Name"
    
    private init() {}
    
    func change(_ name: String) {
        self.name = name
    }
    
}

Note

By the way, you can also create a thread-safe singleton via a Global actor. You can find out how to do this here.

Conclusion

The Singleton Design Pattern is not inherently evil. We don’t live in a perfect world where all relationships are perfectly designed and easy to maintain. In this imperfect world, the Singleton pattern can help us make our codebase more convenient and save time, meeting the needs of the business. All that is required of us is the ability to approach the issue wisely, pre-design our solution, and implement it competently.

That said, we conclude our study of Creational Design Patterns and move on to Structural Patterns.