Singletons in Swift
Part 1 discusses types of singletons that come up in the ios code bases that you should be aware of.
1- Singleton
The Singleton pattern as described in the Design Patterns book (GOF — Gang of Four) by Gamma, Johnson, Vlissides, and Helm is a way to make sure that a class has only one instance and it provides a single point of access to it. The pattern specifies that the class itself should be responsible for keeping track of its sole instance. It can further ensure that no other instance can be created by intercepting requests for creating new objects and providing a way to access the sole instance.
class Singleton {
// static let serves as a lazy loaded constant that cannot be mutated
static let shared = Singleton()
private init() {}
func sayHello() { print("Hello") }
}
Singleton.shared.sayHello() // prints Hello
// Singleton() not allowed
According to the book, a Singleton should also be open for extensions and modifications in the future. Following the book there are 2 ways to achieve this (in Swift) and it’s upto you what you choose based on your tradeoffs:
1- allowing the singleton to be subclassed and overwritten like a normal class so the class cannot be final.
class MediaClient {
static let shared = MediaClient()
private init() {}
func loadMusic(){}
}
class PlayerClient: MediaClient {
func playMusic(){}
override func loadMusic(){
super.loadMusic()
}
}
2- utilizing extension with Swift and allowing the Singleton to be final
final class ImageClient {
static let shared = ImageClient()
private init() {}
func loadImage(){}
}
extension ImageClient {
func loadImageThumbnail(){}
}
2- singleton
Singleton with a lowercase s constitutes a class that is being instantiated only one time in the whole lifecycle of the app; however, its API does not prohibit developers from creating a new instance of the class.
Some examples of such objects are Apple URLSession.shared
and UserDefaults.standard
. Although they offer a shared instance for accessing an immutable reference (get only) of themselves, they also allow their clients to create other instances through their initializers URLSession()
which would create a new instance.
3- Global Mutable State
Global mutable states might look like the same as Singletons however their instances are mutable and can be changed by everyone. This can be of use in some cases but must be taken note of and consider the risks it brings. Notice the Static var shared
class GlobalMutableState {
static var shared = GlobalMutableState()
private(set) var count = 0
func countUp() { count += 1 }
}
class ViewModel {
func doSomething() {
GlobalMutableState.shared.countUp()
GlobalMutableState.shared.countUp()
print(GlobalMutableState.shared.count) //prints 2
GlobalMutableState.shared = GlobalMutableState()
print(GlobalMutableState.shared.count) //prints 0
}
}
Mutable global shared state can be risky as it increases the chances of the system being in inconsistent states. Its state can be changed from any process/thread in the app. However, it offers ease of use when it comes to accessing objects throughout the system. Its trade-offs must be well understood and thought of.
Otherwise, all other classes will be coupled to this global mutable state. This means they’re coupled with the provenance of the data (where the data is). This will make the code more rigid, and error-prone (threading issues or even a class mutating the global data). It’ll also make the code harder and slower to test (you need to mutate a global variable when testing, so you can’t run tests in parallel!). Moreover, if you want to reuse one of the classes in another app, you need to bring the global singleton along.
Examples of good Singleton candidates
So when should we use the singleton pattern?
When we need precisely one instance of a class, and it must be accessible to clients from a well-known access point.
For example, a class that logs messages to the console is a good candidate to do so, as the system may require access to it from any given point. Plus, it’s API is very simple. We should only need its public API to log messages/events, so we don’t need more than one instance or even re-create or mutate its reference in memory.
Moreover, if we need to extend the functionality of that class, then the singleton pattern allows us to subclass or create extensions on the class type.
Examples of bad Singleton candidates
The rule of thumb is to decide which objects should be created just once. Singleton objects should be rare in most systems and need to have a one-to-one relationship with the system. Meaning “it makes sense” or it’s mandatory for a system to have only one “instance of such type.”
For example, Views are bad Singleton candidates as they should be able to allocate and deallocate memory on demand. The same holds for types of components such as Presenters, View Models, and Coordinators.
Dependency Inversion
It’s a common practice for 3rd-party frameworks to provide singleton objects instead of allowing their clients to instantiate internal classes to facilitate their use (creating an instance may be complicated and require private details the framework creator don’t want to expose). Although this approach provides convenience for client developers, if the singleton reference is used throughout the app, it can create a tight coupling between the client and the external framework.
A simple way to break the tight coupling on external frameworks is to use dependency inversion (instead of accessing the concrete singleton instance directly). By hiding the third-party dependency behind an interface that you own and you can extend (e.g., protocol/closure), you can keep the modules of your app agnostic about the implementation details of another external system. Such separation can protect the codebase from breaking changes when updating the external framework, make your code more testable, and also allow you to replace it with another framework in the future easily.
Reference Links:
https://iosacademy.essentialdeveloper.com/p/ios-lead-essentials/
https://www.hackingwithswift.com/example-code/language/what-is-a-singleton/