Singleton vs Dependency Injection in Swift

Matheus C.
Matheus C.
Published February 12, 2021

When coding iOS apps, we often create classes that manage a particular aspect of the application. For example, it's common to develop "manager" classes that encapsulate methods for interacting with a specific application aspect. These aspects commonly include the REST API, WebSockets, database, caching, notifications, chat, etc. That is what's called the Facade pattern, and it's a prevalent way to organize code.

During the creation of a "manager" class, one of the first thoughts is: "How do I access these methods from somewhere else in my app's code, such as inside a view controller?". There are two common ways to go about this: the Singleton and Dependency Injection patterns.

This article will compare the Singleton and Dependency Injection patterns to help you decide which one is best for your use case. For context, we'll use Stream Chat's iOS SDK, which supports both patterns.

Singleton

Image shows the representation of a singleton

Image from https://refactoring.guru/design-patterns/singleton

The Singleton pattern is known for its simplicity. It consists of a Facade class with a public static instance, often called shared, accessible throughout the app's code via MyFacade.shared. Generally, the Facade's class initializer is made private, so the shared instance is the only one in the app.

Check out the snippet below to create a shared instance for Stream Chat's ChatClient class by extending it with the shared static property.

import StreamChat
import UIKit

/// 1: Extend the `ChatClient` class.
extension ChatClient {

    /// 2: Add a `shared` static variable of its own type.
    static var shared: ChatClient!
}

@main
class AppDelegate: UIResponder, UIApplicationDelegate {
    func application(
        _ application: UIApplication,
        didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?

Pros

The main advantage of the singleton pattern is its ease of access. Wherever you need the shared instance, you can access it without modifying your code further.

It's also possible to enforce a single instance if you make the Facade class's initializer private. That is a simple way to avoid common conflicts such as concurrent writes to a database.

Cons

The disadvantages of the singleton pattern become apparent the more complex your app gets. The more places in your app access the shared instance, the more unpredictable your app's behavior becomes and the harder it is to keep all your code in sync with the singleton's global state. Though it can be handy for more uncomplicated use cases, it's often considered an anti-pattern by those who want to be more careful with their code and increase predictability.

It's also possible that limiting the Facade class to a single instance is not what you'll want as your app scales up, and it can be tough to undo that choice.

It's also harder to write unit tests without the possibility of instantiating a mock instance of your class. The last point is not valid for Stream Chat Swift SDK's ChatClient, since its initializer is public to support the Dependency Injection pattern, which we'll discuss next.

Dependency Injection

Image shows the representation of dependency injection

Image from https://stackify.com/dependency-injection/

Dependency Injection can be a great alternative to the Singleton pattern for medium to high complexity apps as it scales with your app with less risk of adding unpredictability. Instead of providing a shared instance that can be accessed without restriction throughout the app, each component or class that needs access to an instance must hold its reference via a parameter or property. This property can be assigned during instantiation or after.

Check out the snippet below to instantiate a ChatClient and a ViewController with the former going into its initializer. Alternatively, we can assign the ViewController's chatClient property after initialization. It would be best if you prefer using the initializer approach to avoid making the property public.

import StreamChat
import UIKit

class SceneDelegate: UIResponder, UIWindowSceneDelegate {
    var window: UIWindow?

    func scene(
        _ scene: UIScene,
        willConnectTo session: UISceneSession,
        options connectionOptions: UIScene.ConnectionOptions
    ) {
        guard let scene = (scene as? UIWindowScene) else { return }

        /// 1: Get the reference to the initial view controller
        let viewController = window?.rootViewController as? ViewController

Pros

There are multiple advantages to the Dependency Injection pattern. Since instantiation is not limited, it's possible to create mock instances to use in unit testing. Additionally, you're not locking yourself out of using multiple instances at the same time in case it's necessary now or in the future.

An injected dependency behavior is also more predictable since its accesses are restricted to pieces of code that explicitly hold an instance to it. Hence, you get to think twice about where to access it.

Dependency Injection doesn't add a global state to your code, so it becomes more reusable. If your code component

Cons

The disadvantages of Dependency Injection are debatable, depending on your use case. For instance, to access the object, you need to add a property or parameter to hold its reference. That can break API in existing projects, if it's something important, or be an additional step in the development process.

With Dependency Injection, it's not possible to enforce a single instance in compile-time. If multiple instances of an object can cause conflicts in runtime, it's necessary to handle those potential issues.

Though not necessarily complicated, some Dependency Injection implementations do get quite robust and can be hard to grasp if you're new to the project.

Which You Should Choose

It depends. Dependency Injection is widely considered the cleaner option, but it can get tricky. Singleton is regarded as an anti-pattern by Clean Code advocates, but it's easy, and it works. Still, developers use both patterns to great success. You should choose the one that fits your current project best while avoiding its common pitfalls.

Finally, as important as choosing the right pattern, don't let your Manager/Facade class become a "god class" with too many responsibilities. Try to make it as single-purposed as possible by interacting with a single aspect of your app.