Singleton – Design Patterns in Swift

The singleton design pattern describes how to solve two problems, one is hiding the constructor of the class, the other is defining a public static operation (e.g. getInstance()) that returns the sole instance of the class.

Singleton Pattern – UML

However, a lot of developers consider the Singleton pattern an antipattern. That’s why its usage is on the decline in Swift code. And it can be recognized by a static creation method, which returns the same cached object.

Basic Singleton

The Singleton class defines a shared static field that lets clients access the unique instance. And the static field that controls the access to the singleton instance. This implementation lets you extend the Singleton class while keeping just one instance of each subclass around.

The Singleton’s initializer should always be private to prevent direct construction calls with the new operator. Finally, any singleton should define some business logic, which can be executed on its instance.

import XCTest

class Singleton {

    static var shared: Singleton = {
        let instance = Singleton()
        // ... configure the instance
        // ...
        return instance
    }()

    private init() {}

    func someBusinessLogic() -> String {

        return "Result of the 'someBusinessLogic' call"
    }
}

Extend the Singleton class and override the copy function to return itself because Singletons should not be cloneable.

extension Singleton: NSCopying {

    func copy(with zone: NSZone? = nil) -> Any {
        return self
    }
}

The client code that figure out only one Singleton instance globally in your app.

class Client {

    static func someClientCode() {
        let instance1 = Singleton.shared
        let instance2 = Singleton.shared

        if (instance1 === instance2) {
            print("Singleton works, both variables contain the same instance.")
        } else {
            print("Singleton failed, variables contain different instances.")
        }
    }
}

Let’s see how it all works together. This is a simple unit test case.

class SingletonConceptual: XCTestCase {

    func testSingletonConceptual() {
        Client.someClientCode()
    }
}

Thread-safe Singleton

Usually, we develop a multi-thread app, but the classic Singleton pattern is not thread-safe in a multi-thread app. Therefore, we need to do something to make it thread-safe.

Assume that we have a Singleton class with a String type field as below.

class Singleton {
    static var shared = Singleton()

    var foo: String = "foo"
}

Obviously, it’s not thread-safe because this is creating some data race issues when using the singleton from different threads. In order to make it thread-safe, we need to override the getter and setter functions of the foo field. Moreover, we have to read and write the value of the foo field in a custom dispatch queue.

class Singleton {

    static let shared = Singleton()

    private init(){}

    private let internalQueue = DispatchQueue(label: "com.singletioninternal.queue",
                                              qos: .default,
                                              attributes: .concurrent)

    private var _foo: String = "hi, guys!"

    var foo: String {
        get {
            return internalQueue.sync {
                _foo
            }
        }
        set (newState) {
            internalQueue.async(flags: .barrier) {
                self._foo = newState
            }
        }
    }
}

Dispatch queues are FIFO (First In First Out) queues to which your application can submit tasks in the form of block objects. It execute tasks either serially or concurrently. Work submitted to dispatch queues executes on a pool of threads managed by the system. Except for the dispatch queue representing your app’s main thread, the system makes no guarantees about which thread it uses to execute a task.

Usually, you schedule work items synchronously or asynchronously. When you schedule a work item synchronously, your code waits until that item finishes execution. When you schedule a work item asynchronously, your code continues executing while the work item runs elsewhere.

But, mostly important, attempting to synchronously execute a work item on the main queue results in deadlock. So, be careful when you use it in your code.

Conclusion

In summary, this pattern has almost the same pros and cons as global variables. Although they’re super-handy, they break the modularity of your code. You can’t just use a class that depends on it in some other context. You’ll have to carry it as well. Most of the time, this limitation comes up during the creation of unit tests.

Leave a Reply

Your email address will not be published. Required fields are marked *