Design Patterns in Swift: Singleton
What is Singleton Singleton is a pattern that creates a single 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 consider Singleton an anti-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 create Singletons. That's because Swift is a language that you don't need to specify any kind of higher-level scope, such as Namespaces or Packages. Every .swift file included in your bundle has visibility from any other .swift file in 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: import SwiftUI // Any file in the entire module can now access `myGlobalObj` let myGlobalObj = MyCustomClass() struct HomeView: View { // .. } However, this is not considered a Singleton yet. A Singleton instance must be a static property (or a class 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 a Singleton in Swift: MySingletonObject.swift file class MySingletonObject { static let shared = MySingletonObject() var message = "" } This allows you to access it (read and modify) from anywhere in your module: HomeView.swift struct HomeView: View { var body: some View { Button("Tap Me!") { MySingletonObject.shared.message = "Hello, World!" } } } SettingsView.swift //.. struct SettingsView: View { var body: some View { Button("Print Singleton Value") { print(MySingletonObject.shared.message) } } } //.. The value set on HomeView can be printed in SettingsView, 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 called shared. 3. Is Any static 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: let defaultFileManager = FileManager.default let standardUserDefaults = UserDefaults.standard If you jump to the definition of any of these properties, you'll see something like: open class UserDefaults : NSObject { open class var standard: UserDefaults { get } // .. } ``` {% endraw %} That's very similar to what we saw that is a **Singleton**, but it has an important difference: it's not the only **possible** instance of this type. {% raw %} ```swift let systemSettingsStorage = UserDefaults(suiteName: "system-settings") With UserDefaults, for example, you can have multiple instances of that type in the same application. So, default and standard are names used to static properties that hold instance shorthands of a type, but not Singletons. When creating Singletons, prefer shared that is the current convention. In legacy code you can also find it as sharedInstance. Impact on Unit Testing As we saw in the last section, Singleton objects are usually held in static properties. And it's almost common sense in software engineering that making static calls in the middle of the business logic of your software is a bad practice. class UserStorage { static let shared = UserStorage() private init() {} func saveUser(name: String) { print("Saving user: \(name) to persistent storage") // Saves the user... } func fetchUser(name: String) -> User? { // Fetches the user from the storage } } class UserManager { func registerUser(name: String) -> Bool { // Checking if the user exists already if let _ = UserStorage.shared.fetchUser(name: name) { print("User already exists") return false } UserStorage.shared.saveUser(name: name) print("User registered successfully") return true } } 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 the UserManager 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 alre

What is Singleton
Singleton is a pattern that creates a single 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 consider Singleton an anti-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 create Singletons. That's because Swift is a language that you don't need to specify any kind of higher-level scope, such as Namespaces or Packages. Every .swift
file included in your bundle has visibility from any other .swift
file in 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:
import SwiftUI
// Any file in the entire module can now access `myGlobalObj`
let myGlobalObj = MyCustomClass()
struct HomeView: View {
// ..
}
However, this is not considered a Singleton yet. A Singleton instance must be a static property (or a class 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 a Singleton in Swift:
MySingletonObject.swift file
class MySingletonObject {
static let shared = MySingletonObject()
var message = ""
}
This allows you to access it (read and modify) from anywhere in your module:
HomeView.swift
struct HomeView: View {
var body: some View {
Button("Tap Me!") {
MySingletonObject.shared.message = "Hello, World!"
}
}
}
SettingsView.swift
//..
struct SettingsView: View {
var body: some View {
Button("Print Singleton Value") {
print(MySingletonObject.shared.message)
}
}
}
//..
The value set on HomeView
can be printed in SettingsView
, 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 called shared
.
3. Is Any static
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:
let defaultFileManager = FileManager.default
let standardUserDefaults = UserDefaults.standard
If you jump to the definition of any of these properties, you'll see something like:
open class UserDefaults : NSObject {
open class var standard: UserDefaults { get }
// ..
} ```
{% endraw %}
That's very similar to what we saw that is a **Singleton**, but it has an important difference: it's not the only **possible** instance of this type.
{% raw %}
```swift
let systemSettingsStorage = UserDefaults(suiteName: "system-settings")
With UserDefaults
, for example, you can have multiple instances of that type in the same application.
So, default
and standard
are names used to static properties that hold instance shorthands of a type, but not Singletons. When creating Singletons, prefer shared
that is the current convention. In legacy code you can also find it as sharedInstance
.
Impact on Unit Testing
As we saw in the last section, Singleton objects are usually held in static properties. And it's almost common sense in software engineering that making static calls in the middle of the business logic of your software is a bad practice.
class UserStorage {
static let shared = UserStorage()
private init() {}
func saveUser(name: String) {
print("Saving user: \(name) to persistent storage")
// Saves the user...
}
func fetchUser(name: String) -> User? {
// Fetches the user from the storage
}
}
class UserManager {
func registerUser(name: String) -> Bool {
// Checking if the user exists already
if let _ = UserStorage.shared.fetchUser(name: name) {
print("User already exists")
return false
}
UserStorage.shared.saveUser(name: name)
print("User registered successfully")
return true
}
}
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 the UserManager
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:
struct UserTests {
let userManager = UserManager()
@Test("Test that registering an existing user should fail")
func duplicateUserSaving() async throws {
// Saving "John" for the first time
let _ = userManager.registerUser(name: "John")
// Saving "John" for the second time
let secondRegistrationResult = userManager.registerUser(name: "John")
#expect(secondRegistrationResult == false)
}
}
If you're not familiar with the code above, I highly recommend you to read my post about Swift 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:
struct UserTests {
let userManager = UserManager()
@Test("Test that a duplicate user cannot be saved")
func duplicateUserSaving() async throws {
// ..
}
@Test("Test that a a new user can successfully be saved")
func newUserSaving() async throws {
let registrationResult = userManager.registerUser(name: "John")
#expect(registrationResult == true)
}
}
If the duplicateUserSaving
test case runs first, and if it writes to a file in disk, the newUserSaving
test may read from this storage and incorrectly fail. This means that your test is not atomic (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 of static 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 using Singletons. For that, we need to associate Singleton with another design pattern: Dependency Injection.
With Dependency Injection, you can make your business logic be coupled to an abstraction, and then change the Singleton instance to another thing that makes more sense depending on the context (a mock in testing environment, for example).
Firstly, you need to create the abstraction, we will use a protocol for that:
protocol UserStorage {
func saveUser(name: String)
func fetchUser(name: String) -> User?
}
Then, you adapt your Singleton to conform to your protocol. Let's say that our storage class was saving the data in a Realm database:
class RealmUserStorage: UserStorage {
static let shared: UserStorage = RealmUserStorage()
// ..
}
Now, you adapt your UserManager
class to depend on the protocol instead of the Singleton. Note that by making the Singleton the default value in the initializer, it eliminates the need of specifying a value explicitly:
class UserManager {
// Created a stored property for the storage object
let userStorage: UserStorage
// Receive it in the initializer, but with the Singleton as default
init(userStorage: UserStorage = RealmUserStorage.shared) {
self.userStorage = userStorage
}
func registerUser(name: String) -> Bool {
// Calling the class property instead of directly the Singleton
if let _ = userStorage.fetchUser(name: name) {
print("User already exists")
return false
}
// Calling the class property instead of directly the Singleton
userStorage.saveUser(name: name)
print("User registered successfully")
return true
}
}
// Even though the initializer can receive a value, if not passing an attribute I use the default storage system, that is Realm
let userManager = UserManager()
Now, you're able to mock the UserStorage
behavior in a test environment, removing the dependency on the storage itself and testing only the Business Logic:
class MockUserStorage: UserStorage {
var existingUserName = ""
func saveUser(name: String) { }
func fetchUser(name: String) -> User? {
// Simulating a stored user
if name == existingUserName {
return User(name: name)
}
return nil
}
}
And in your test environment, you can inject the mock in the UserManager
object, like that:
struct UserTests {
let userStorage: MockUserStorage
let userManager: UserManager
init() {
userStorage = MockUserStorage()
userManager = UserManager(userStorage: userStorage)
}
@Test("Test that a duplicate user cannot be saved")
func duplicateUserSaving() async throws {
let userName = "John"
// Simulating an existing user
userStorage.existingUserName = userName
// Saving duplicate user
let registrationResult = userManager.registerUser(name: userName)
#expect(registrationResult == false)
}
}
This way, you have a testable code that uses a Singleton. You also have the benefit of a modular code, if you want to change the RealmUserStorage
in the future with a SQLiteUserStorage
, CoreDataUserStorage
or any other framework, you don't need to change anything in your business logic.
Best Practices
We said previously that FileManager.default
and UserDefaults.standard
cannot be considered Singletons since they are not the only instances of their types. You could guarantee that a Singleton in your code will be the only instance of its type by making its initializer private
:
class RealmUserStorage: UserStorage {
// The `init()` being private makes it visible only inside the class scope
private init() {}
// Here the initializer is vibisle
static let shared: UserStorage = RealmUserStorage()
}
let anotherInstance = RealmUserStorage() // ❌ 'RealmUserStorage' initializer is inaccessible due to 'private' protection level
If any developer tries to initialize a new instance of RealmUserStorage
, Xcode will prompt a build error, making sure that the type is only used from the Singleton.
Impact in Memory
A common usage for Singletons is in-memory caching. Imagine a collection of UI element in which the user navigate back and forth, and each element displays an image download from the internet. You can have a caching system to avoid downloading the same image multiple times:
class ImageCache {
static let shared = ImageCache() // Singleton instance
private var cache: [String: UIImage] = [:]
private init() {}
func getImage(from urlString: String) async -> UIImage? {
// Check if the image is already cached
if let cachedImage = cache[urlString] {
return cachedImage
}
// If not cached, attempt to download the image
guard let image = await downloadImage(from: urlString) else {
return nil
}
// Save the image in cache
cache[urlString] = image
return image
}
}
This kind of implementation is very useful, but it can introduce so flaws to your app:
1. Increasing Memory Usage
The cache
property, being a Dictionary
, will hold the cached images for all the object lifecycle, and since it's a Singleton, that means all the app 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 the Singleton state may receive undesired results. I talked more about data races and how to avoid then using actors
in this post, take a look!
A simple solution, that would not require Actors, would be making the cache
property of type NSCache
:
private let cache = NSCache<NSString, UIImage>()
Then you replace:
// Retrieving cached image
let cachedImage = cache[urlString]
// Caching an image
cache[urlString] = image
With:
// Retrieving cached image
let cachedImage = cache.object(forKey: urlString as NSString)
// Caching an image
cache.setObject(image, forKey: urlString as NSString)
Using a NSCache
brings several benefits: Swift automatically removes cached data in run-time if memory is low, the NSCache
type is automatically thread-safe, and it don't create strong 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 an actor
with a NSCache
property, allowing you to add logic as expiration and prefetching.
Conclusion
The Singleton pattern is useful for sharing state across an application but comes with potential drawbacks. To address these, we can apply Dependency 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.
- Using protocols 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.