— Swift, Dependency Injection, Factory, iOS Development — 1 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.
Factory is a lightweight, flexible, and powerful dependency injection library for Swift. It offers several key advantages:
Let's explore how to use Factory in your Swift projects.
To define a dependency, you add a computed property to the Container
extension:
1import Factory2
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.
Factory offers several ways to inject dependencies. One common method is using the @Injected
property wrapper:
1class WeatherViewModel: ObservableObject {2 @Injected(\.weatherService) private var weatherService3 4 func fetchWeather(for city: String) {5 weatherService.getWeather(for: city) { result in6 // Handle the result7 }8 }9}
For iOS 17 and later, using the new Observation framework:
1@Observable class WeatherViewModel {2 @ObservationIgnored3 @Injected(\.weatherService) private var weatherService4 5 func fetchWeather(for city: String) {6 weatherService.getWeather(for: city) { result in7 // Handle the result8 }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 in6 // Handle the result7 }8 }9}
Factory provides different scopes to manage the lifecycle of your dependencies:
1extension Container {2 var locationManager: Factory<LocationManaging> { 3 self { GPSLocationManager() }4 .singleton5 }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 itContexts 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.
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}
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.