
What is Singleton
Singleton is a pattern that creates asingle instance of a type for the entire application, usually in a global scope (with visibility from any place in the code). The idea of having an object like that is to share state and/or behavior across different layers. Although it's very effective at doing that, many considerSingleton ananti-pattern, due to its side-effects. In today's article, we're going to see how to use this pattern in Swift applications, why some engineers don't like it, and the best strategies to avoid its disadvantages.
How to Create a Singleton
In Swift, it's very easy to createSingletons. That's because Swift is a language that you don't need to specify any kind of higher-level scope, such asNamespaces orPackages. Every.swift
file included in your bundle has visibility from any other.swift
filein the same module.
1. Creating a Globally-Visible Object in Swift
Simply creating a variable in a global scope makes this variable visible in the entire module in Swift:
importSwiftUI// Any file in the entire module can now access `myGlobalObj`letmyGlobalObj=MyCustomClass()structHomeView:View{// ..}
However, this is not considered aSingleton yet. ASingleton instance must be astatic property (or aclass var
) of a type, that is immediately initialized when the application launches, guaranteeing that the object will be alive during all its lifecycle and it's the only instance of that concrete type in the entire application.
2. Creating a Singleton
That's the most basic implementation of aSingleton in Swift:
MySingletonObject.swift file
classMySingletonObject{staticletshared=MySingletonObject()varmessage=""}
This allows you to access it (read and modify) from anywhere in your module:
HomeView.swift
structHomeView:View{varbody:someView{Button("Tap Me!"){MySingletonObject.shared.message="Hello, World!"}}}
SettingsView.swift
//..structSettingsView:View{varbody:someView{Button("Print Singleton Value"){print(MySingletonObject.shared.message)}}}//..
The value set onHomeView
can be printed inSettingsView
, without any of the views "knowing" each other.
You could name your static property anything, but that doesn’t mean you should. There's a convention in Swift that properties that represents a singleton should be calledshared
.
3. Is Anystatic
Property in a Type, That Holds an Instance of the Type Itself, a Singleton?
In Apple development, is common to see things very similar to a singleton:
letdefaultFileManager=FileManager.defaultletstandardUserDefaults=UserDefaults.standard
If you jump to the definition of any of these properties, you'll see something like:
openclassUserDefaults:NSObject{openclassvarstandard:UserDefaults{get}// ..}
That's very similar to what we saw that is aSingleton, but it has an important difference: it's not the onlypossible instance of this type.
letsystemSettingsStorage=UserDefaults(suiteName:"system-settings")
WithUserDefaults
, for example, you can have multiple instances of that type in the same application.
So,default
andstandard
are names used to static properties that hold instanceshorthands of a type, but notSingletons. When creatingSingletons, prefershared
that is the current convention. In legacy code you can also find it assharedInstance
.
Impact on Unit Testing
As we saw in the last section,Singleton objects are usually held instatic properties. And it's almost common sense in software engineering that makingstatic calls in the middle of the business logic of your software is a bad practice.
classUserStorage{staticletshared=UserStorage()privateinit(){}funcsaveUser(name:String){print("Saving user:\(name) to persistent storage")// Saves the user...}funcfetchUser(name:String)->User?{// Fetches the user from the storage}}classUserManager{funcregisterUser(name:String)->Bool{// Checking if the user exists alreadyiflet_=UserStorage.shared.fetchUser(name:name){print("User already exists")returnfalse}UserStorage.shared.saveUser(name:name)print("User registered successfully")returntrue}}
The above's piece of code introduces a tight coupling between the business logic of the app and the storage service.
Imagine that you have to write some unit tests theUserManager
class. You have then two test cases:
- Trying to register an existing user should fail.
- Trying to register a new user should successfully save it.
Inside the testing environment, how could you simulate the scenario which there's already an user in the storage? You'd need to save it first:
structUserTests{letuserManager=UserManager()@Test("Test that registering an existing user should fail")funcduplicateUserSaving()asyncthrows{// Saving "John" for the first timelet_=userManager.registerUser(name:"John")// Saving "John" for the second timeletsecondRegistrationResult=userManager.registerUser(name:"John")#expect(secondRegistrationResult == false)}}
If you're not familiar with the code above, I highly recommend you to read my post aboutSwift Testing, the new Apple's Testing Framework
The test above, despite possibly creating problems in CI/CD environments, probably will work locally. But then, if you create another test for other test case, you may face problems:
structUserTests{letuserManager=UserManager()@Test("Test that a duplicate user cannot be saved")funcduplicateUserSaving()asyncthrows{// ..}@Test("Test that a a new user can successfully be saved")funcnewUserSaving()asyncthrows{letregistrationResult=userManager.registerUser(name:"John")#expect(registrationResult == true)}}
If theduplicateUserSaving
test case runs first, and if it writes to a file in disk, thenewUserSaving
test may read from this storage and incorrectly fail. This means that your test is notatomic (one test case is interfering in the other).
Also, as mentioned before, the testing environment can possibly not have access to all the low-level API required to make the storage works. Keep in mind that some hardware resources are only available on a physical device and not in a simulated environment.
How to Create Testable Code with Singletons
We just saw that the usage ofstatic code in general (Singletons included) can be bad for testing and decoupling. But that doesn't mean you cannot work around these problems. There are some robust strategies to have modular and testable code usingSingletons. For that, we need to associateSingleton with another design pattern:Dependency Injection.
WithDependency Injection, you can make your business logic be coupled to anabstraction, and then change theSingleton instance to another thing that makes more sense depending on the context (a mock in testing environment, for example).
Firstly, you need to create theabstraction, we will use aprotocol for that:
protocolUserStorage{funcsaveUser(name:String)funcfetchUser(name:String)->User?}
Then, you adapt yourSingleton to conform to yourprotocol. Let's say that our storage class was saving the data in aRealm database:
classRealmUserStorage:UserStorage{staticletshared:UserStorage=RealmUserStorage()// ..}
Now, you adapt yourUserManager
class to depend on theprotocol instead of theSingleton. Note that by making theSingleton the default value in the initializer, it eliminates the need of specifying a value explicitly:
classUserManager{// Created a stored property for the storage objectletuserStorage:UserStorage// Receive it in the initializer, but with the Singleton as defaultinit(userStorage:UserStorage=RealmUserStorage.shared){self.userStorage=userStorage}funcregisterUser(name:String)->Bool{// Calling the class property instead of directly the Singletoniflet_=userStorage.fetchUser(name:name){print("User already exists")returnfalse}// Calling the class property instead of directly the SingletonuserStorage.saveUser(name:name)print("User registered successfully")returntrue}}// Even though the initializer can receive a value, if not passing an attribute I use the default storage system, that is RealmletuserManager=UserManager()
Now, you're able tomock theUserStorage
behavior in a test environment, removing the dependency on the storage itself and testing only theBusiness Logic:
classMockUserStorage:UserStorage{varexistingUserName=""funcsaveUser(name:String){}funcfetchUser(name:String)->User?{// Simulating a stored userifname==existingUserName{returnUser(name:name)}returnnil}}
And in your test environment, you can inject the mock in theUserManager
object, like that:
structUserTests{letuserStorage:MockUserStorageletuserManager:UserManagerinit(){userStorage=MockUserStorage()userManager=UserManager(userStorage:userStorage)}@Test("Test that a duplicate user cannot be saved")funcduplicateUserSaving()asyncthrows{letuserName="John"// Simulating an existing useruserStorage.existingUserName=userName// Saving duplicate userletregistrationResult=userManager.registerUser(name:userName)#expect(registrationResult == false)}}
This way, you have a testable code that uses aSingleton. You also have the benefit of amodular code, if you want to change theRealmUserStorage
in the future with aSQLiteUserStorage
,CoreDataUserStorage
or any other framework, you don't need to change anything in your business logic.
Best Practices
We said previously thatFileManager.default
andUserDefaults.standard
cannot be consideredSingletons since they are not the only instances of their types. You could guarantee that aSingleton in your code will be the only instance of its type by making its initializerprivate
:
classRealmUserStorage:UserStorage{// The `init()` being private makes it visible only inside the class scopeprivateinit(){}// Here the initializer is vibislestaticletshared:UserStorage=RealmUserStorage()}letanotherInstance=RealmUserStorage()// ❌ 'RealmUserStorage' initializer is inaccessible due to 'private' protection level
If any developer tries to initialize a new instance ofRealmUserStorage
, Xcode will prompt a build error, making sure that the type is only used from theSingleton.
Impact in Memory
A common usage forSingletons is in-memory caching. Imagine a collection of UI element in which the user navigate back and forth, and each element displays an image downloaded from the internet. You can have a caching system to avoid downloading the same image multiple times:
classImageCache{staticletshared=ImageCache()// Singleton instanceprivatevarcache:[String:UIImage]=[:]privateinit(){}funcgetImage(fromurlString:String)async->UIImage?{// Check if the image is already cachedifletcachedImage=cache[urlString]{returncachedImage}// If not cached, attempt to download the imageguardletimage=awaitdownloadImage(from:urlString)else{returnnil}// Save the image in cachecache[urlString]=imagereturnimage}}
This kind of implementation is very useful, but it can introduce so flaws to your app:
1. Increasing Memory Usage
Thecache
property, being aDictionary
, will hold the cached images for all the object lifecycle, and since it's aSingleton, that means all theapp lifecycle. If several large images are cached, you may face slow perfomance or a stack overflow.
2. Data races
This piece of code above is not thread-safe. That means that multiple threads accessing the theSingleton state may receive undesired results. I talked more about data races and how to avoid then usingactors
inthis post, take a look!
A simple solution, that would not requireActors, would be making thecache
property of typeNSCache
:
privateletcache=NSCache<NSString,UIImage>()
Then you replace:
// Retrieving cached imageletcachedImage=cache[urlString]// Caching an imagecache[urlString]=image
With:
// Retrieving cached imageletcachedImage=cache.object(forKey:urlStringasNSString)// Caching an imagecache.setObject(image,forKey:urlStringasNSString)
Using aNSCache
brings several benefits: Swift automatically removes cached data in run-time if memory is low, theNSCache
type is automatically thread-safe, and it don't createstrong references to the keys, meaning that ARC automatically free space when the object key is not being referenced anywhere else.
For a more robust implementation, you could combine anactor
with aNSCache
property, allowing you to add logic asexpiration andprefetching.
Conclusion
TheSingleton pattern is useful for sharing state across an application but comes with potential drawbacks. To address these, we can applyDependency Injection for better flexibility, testability, and maintainability.
Key takeaways:
- Singletons are useful for shared state but can lead to tight coupling and hidden dependencies.
- Dependency Injection helps decouple components and improve testability.
- Usingprotocols instead of singletons enhances modularity and scalability.
By carefully applying the Singleton pattern and considering alternatives, you can write more maintainable and flexible Swift code.
Top comments(0)
For further actions, you may consider blocking this person and/orreporting abuse