Skip to content
DeveloperMemos

Using Factory for Dependency Injection in Swift

Swift, Dependency Injection, Factory, iOS Development1 min read

Dependency Injection (DI) is a crucial design pattern in modern software development, promoting loose coupling, improved testability, and better code organization. In the Swift ecosystem, the Factory library has emerged as a powerful tool for implementing DI, drawing inspiration from SwiftUI and offering a range of features that make it particularly well-suited for iOS and macOS development.

What is Factory?

Factory is a lightweight, flexible, and powerful dependency injection library for Swift. It offers several key advantages:

  1. Adaptable: Supports various dependency injection strategies and techniques.
  2. Powerful: Provides features like containers, scopes, passed parameters, contexts, and decorators.
  3. Performant: Requires minimal setup time and offers fast resolution.
  4. Safe: Ensures compile-time safety for dependency resolution.
  5. Concise: Allows for simple, one-line registrations and resolutions.
  6. Flexible: Compatible with various architectures (MVVM, MVP, Clean, VIPER) and platforms (iOS, macOS).

Let's explore how to use Factory in your Swift projects.

Basic Usage

Defining Dependencies

To define a dependency, you add a computed property to the Container extension:

1import Factory
2
3extension Container {
4 var weatherService: Factory<WeatherServiceProtocol> {
5 self { LiveWeatherService() }
6 }
7}

This creates a factory for WeatherServiceProtocol that returns a new instance of LiveWeatherService when called.

Injecting Dependencies

Factory offers several ways to inject dependencies. One common method is using the @Injected property wrapper:

1class WeatherViewModel: ObservableObject {
2 @Injected(\.weatherService) private var weatherService
3
4 func fetchWeather(for city: String) {
5 weatherService.getWeather(for: city) { result in
6 // Handle the result
7 }
8 }
9}

For iOS 17 and later, using the new Observation framework:

1@Observable class WeatherViewModel {
2 @ObservationIgnored
3 @Injected(\.weatherService) private var weatherService
4
5 func fetchWeather(for city: String) {
6 weatherService.getWeather(for: city) { result in
7 // Handle the result
8 }
9 }
10}

You can also resolve dependencies directly from the container:

1class WeatherViewModel: ObservableObject {
2 private let weatherService = Container.shared.weatherService()
3
4 func fetchWeather(for city: String) {
5 weatherService.getWeather(for: city) { result in
6 // Handle the result
7 }
8 }
9}

Advanced Features

Scopes

Factory provides different scopes to manage the lifecycle of your dependencies:

1extension Container {
2 var locationManager: Factory<LocationManaging> {
3 self { GPSLocationManager() }
4 .singleton
5 }
6
7 var cacheService: Factory<CacheServiceProtocol> {
8 self { InMemoryCacheService() }
9 .scope(.session)
10 }
11}

The available scopes include:

  • .unique (default): Creates a new instance each time
  • .singleton: Creates a single instance for the lifetime of the application
  • .cached: Persists until the cache is reset
  • .shared: Exists as long as there's a strong reference to it

Contexts

Contexts allow you to register overrides for specific scenarios:

1container.pushNotificationService.onDebug {
2 MockPushNotificationService()
3}

This example registers a mock push notification service when running in debug mode.

Mocking for Testing and Previews

Factory makes it easy to replace dependencies with mocks for testing or SwiftUI previews:

1struct WeatherView_Previews: PreviewProvider {
2 static var previews: some View {
3 let _ = Container.shared.weatherService.register { MockWeatherService() }
4 WeatherView()
5 }
6}

For unit tests:

1final class WeatherViewModelTests: XCTestCase {
2 override func setUp() {
3 super.setUp()
4 Container.shared.reset()
5 }
6
7 func testFetchWeather() {
8 Container.shared.weatherService.register { MockWeatherService(mockData: .sunny) }
9 let viewModel = WeatherViewModel()
10 viewModel.fetchWeather(for: "London")
11 XCTAssertEqual(viewModel.currentWeather, .sunny)
12 }
13}

Conclusion

Factory provides a robust and flexible solution for dependency injection in Swift, particularly well-suited for SwiftUI and UIKit applications. Its ease of use, powerful features, and compatibility with various architectural patterns make it an excellent choice for managing dependencies in your iOS and macOS projects.

By leveraging Factory's capabilities, you can write more modular, testable, and maintainable code. As your projects grow in complexity, the benefits of using a DI framework like Factory become increasingly apparent, allowing you to focus on building features rather than managing object creation and lifecycles.