Singleton vs Dependency Injection in iOS Swift

Ranga C
4 min readJul 24, 2023

--

Introduction: When developing iOS applications in Swift, one critical decision developers face is how to manage the creation and lifecycle of objects efficiently. Two popular design patterns that address this concern are Singleton and Dependency Injection. In this article, we will delve into the concepts of Singleton and Dependency Injection, examine their advantages and disadvantages, and provide insights into choosing the most appropriate pattern for your iOS Swift projects.

Singleton Pattern: The Singleton pattern ensures that a class has only one instance and provides a global point of access to that instance. This pattern is achieved by defining a static property to hold the single instance of the class and a private initializer to prevent external instantiation.

Advantages of Singleton:

  • Global Accessibility: A Singleton instance can be accessed from anywhere within the application, making it convenient for sharing resources and managing centralized data.
  • Lazy Initialization: Singleton instances are created only when accessed for the first time, saving memory and resources until they are actually needed.

Disadvantages of Singleton:

  • Tight Coupling: Overuse of Singleton can lead to tight coupling between classes, making it difficult to test and maintain the codebase.
  • Global State: Singleton introduces global state, which can lead to unintended side effects and make debugging complex.

Dependency Injection: Dependency Injection is a pattern where dependencies (objects or services) are provided to a class from the outside, rather than being created internally. This is accomplished by passing the dependencies as parameters to the class’s initializer or as properties that can be set from outside.

Advantages of Dependency Injection:

  • Decoupling: By passing dependencies from outside, Dependency Injection promotes loose coupling between classes, making code more modular and easier to test.
  • Flexibility: Dependency Injection allows different implementations of dependencies to be easily swapped, enhancing code flexibility and maintainability.

Disadvantages of Dependency Injection:

  • Increased Boilerplate Code: Implementing Dependency Injection can lead to more code for passing dependencies around, potentially making the codebase verbose.
  • Complexity: In large projects, managing dependencies can become complex, requiring careful planning and organization.

Choosing the Right Approach:

  1. Singleton is suitable when:
  • There is a genuine need for only one instance of a class throughout the application (e.g., managing app configuration, logging).
  • Global accessibility is essential for sharing common data across multiple components.

2 . Dependency Injection is recommended when:

  • There is a need to promote decoupling and testability in the application.
  • Different implementations of dependencies might be required (e.g., using mock objects during testing).
  • You want to avoid global state and reduce the risk of hidden dependencies.

Best Practices:

  • Use Singleton sparingly and only when it is genuinely necessary for global accessibility and resource management.
  • Embrace Dependency Injection to foster decoupling and flexibility in your codebase.
  • Consider using Dependency Injection frameworks like Swinject or DaggerSwift to simplify the management of dependencies.

let’s add an example to illustrate the difference between using Singleton and Dependency Injection in iOS Swift.

Example Scenario: Suppose we are developing a weather app that fetches weather data from different APIs based on user preferences. The app consists of two classes: WeatherService to handle data retrieval and SettingsManager to manage user preferences.

Singleton Approach:

class WeatherService {
static let shared = WeatherService()
private init() {}

func fetchWeatherData() -> WeatherData {
// Implementation to fetch weather data from the selected API
// ...
return weatherData
}
}

class SettingsManager {
static let shared = SettingsManager()
private init() {}

var selectedAPI: WeatherAPI = .openWeather // Default API

func updateSelectedAPI(api: WeatherAPI) {
self.selectedAPI = api
}
}

In this example, both WeatherService and SettingsManager are implemented as Singletons. The WeatherService instance is globally accessible and used to fetch weather data. The SettingsManager is also a Singleton, allowing the user to update the selected API.

Usage of Singleton in the App:

// Fetch weather data using Singleton
let weatherData = WeatherService.shared.fetchWeatherData()

// Update selected API using Singleton
SettingsManager.shared.updateSelectedAPI(api: .weatherAPIX)

Dependency Injection Approach:

class WeatherService {
let apiClient: WeatherAPIClient

init(apiClient: WeatherAPIClient) {
self.apiClient = apiClient
}

func fetchWeatherData() -> WeatherData {
// Implementation to fetch weather data using the provided API client
// ...
return weatherData
}
}

class SettingsManager {
var selectedAPI: WeatherAPI = .openWeather // Default API

func updateSelectedAPI(api: WeatherAPI) {
self.selectedAPI = api
}
}

In this example, we use Dependency Injection to pass the WeatherAPIClient to the WeatherService. The WeatherService no longer holds a reference to a specific API client implementation, making it more flexible.

Usage of Dependency Injection in the App:

// Create WeatherAPIClient based on user preference
let apiClient = createAPIClientFromPreference()

// Inject the WeatherAPIClient into WeatherService
let weatherService = WeatherService(apiClient: apiClient)

// Fetch weather data using Dependency Injection
let weatherData = weatherService.fetchWeatherData()

// Update selected API
let settingsManager = SettingsManager()
settingsManager.updateSelectedAPI(api: .weatherAPIX)

In this example, the WeatherService now relies on the injected WeatherAPIClient, which can be created based on the user's preference (e.g., openWeather or weatherAPIX). The SettingsManager remains independent and does not follow the Singleton pattern.

Conclusion: The Singleton and Dependency Injection patterns offer different approaches for managing object creation and dependencies in iOS Swift applications. The Singleton pattern provides global accessibility but may lead to tight coupling and global state. In contrast, Dependency Injection promotes decoupling and flexibility, making it easier to test and maintain code. By carefully considering the specific requirements of your app, you can choose the most suitable approach to create a robust and efficient iOS Swift application.

--

--