Table of Contents
- Introduction to Dependency Injection (DI)
- Why DI Matters: Testability & Reusability
- Types of Dependency Injection
- Implementing Dependency Injection in Swift (with MapKit Example)
- Unit Testing with Dependency Injection
- Conclusion
⸻
Dependency Injection: Improving Code Testability and Reusability
1. Introduction to Dependency Injection (DI)
Dependency Injection (DI) is a design pattern that helps decouple components in an application, making code more modular, testable, and reusable. Instead of a class creating its dependencies, they are provided from the outside.
2. Why DI Matters: Testability & Reusability
• Improves Testability: Dependencies can be replaced with mocks or stubs during testing.
• Enhances Reusability: Components can be reused in different contexts.
• Reduces Coupling: Classes become independent of specific implementations.
For example, in an iPhone Maps App, we might have a LocationService class that fetches user locations. Without DI, this class might directly create a CLLocationManager instance, making it difficult to replace or mock in tests. With DI, we inject the dependency, allowing us to substitute a mock service when testing.
⸻
3. Types of Dependency Injection
There are three main types of DI:
- Constructor Injection: Dependencies are passed via the initializer.
- Property Injection: Dependencies are assigned after object creation.
- Method Injection: Dependencies are passed via method parameters.
In Swift, constructor injection is commonly used.
⸻
4. Implementing Dependency Injection in Swift (MapKit Example)
We’ll build a Maps App for iPhone that uses MapKit to display the user’s location. We’ll apply Dependency Injection to inject the location service into our MapViewModel.
Step 1: Create a New iOS Project in Xcode
- Open Xcode → Create a new iOS App.
- Choose “App” and select Swift + UIKit.
- Name the project “DIMapApp”.
Step 2: Setup Core Components
1. Define the LocationServiceProtocol
import CoreLocation
protocol LocationServiceProtocol {
func requestLocation()
var locationUpdated: ((CLLocation) -> Void)? { get set }
}
2. Create a Concrete Implementation of LocationService
class LocationService: NSObject, LocationServiceProtocol, CLLocationManagerDelegate {
private let locationManager = CLLocationManager()
var locationUpdated: ((CLLocation) -> Void)?
override init() {
super.init()
locationManager.delegate = self
locationManager.requestWhenInUseAuthorization()
}
func requestLocation() {
locationManager.requestLocation()
}
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
guard let location = locations.last else { return }
locationUpdated?(location)
}
func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
print("Location error: \(error.localizedDescription)")
}
}
Step 3: Inject the Dependency into a ViewModel
import Foundation
import CoreLocation
class MapViewModel {
private let locationService: LocationServiceProtocol
var onLocationUpdate: ((CLLocation) -> Void)?
init(locationService: LocationServiceProtocol) {
self.locationService = locationService
self.locationService.locationUpdated = { [weak self] location in
self?.onLocationUpdate?(location)
}
}
func fetchLocation() {
locationService.requestLocation()
}
}
Implementation in Swift with MapKit
Here's how to implement Dependency Injection in a Swift application using MapKit.
import MapKit
class MapViewController: UIViewController {
var mapView: MKMapView
// Constructor Injection
init(mapView: MKMapView) {
self.mapView = mapView
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
Unit Testing
When unit testing components that use DI, you can easily mock dependencies.
class MockMapView: MKMapView {
// Mock implementation
}
func testMapViewController() {
let mockMapView = MockMapView()
let mapVC = MapViewController(mapView: mockMapView)
// Perform tests on mapVC
}
Step 4: Integrate MapKit into the ViewController
import UIKit
import MapKit
class MapViewController: UIViewController {
private var mapView: MKMapView!
private var viewModel: MapViewModel!
override func viewDidLoad() {
super.viewDidLoad()
setupMapView()
setupViewModel()
}
private func setupMapView() {
mapView = MKMapView(frame: view.bounds)
mapView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
view.addSubview(mapView)
}
private func setupViewModel() {
let locationService = LocationService()
viewModel = MapViewModel(locationService: locationService)
viewModel.onLocationUpdate = { [weak self] location in
DispatchQueue.main.async {
let region = MKCoordinateRegion(
center: location.coordinate,
latitudinalMeters: 500,
longitudinalMeters: 500
)
self?.mapView.setRegion(region, animated: true)
}
}
viewModel.fetchLocation()
}
}
5. Unit Testing with Dependency Injection
With DI, we can mock the location service to test MapViewModel.
Step 1: Create a Mock Location Service
class MockLocationService: LocationServiceProtocol {
var locationUpdated: ((CLLocation) -> Void)?
func requestLocation() {
let mockLocation = CLLocation(latitude: 37.7749, longitude: -122.4194) // San Francisco
locationUpdated?(mockLocation)
}
}
Step 2: Write a Unit Test
import XCTest
import CoreLocation
class MapViewModelTests: XCTestCase {
func testLocationUpdate() {
let mockService = MockLocationService()
let viewModel = MapViewModel(locationService: mockService)
let expectation = self.expectation(description: "Location update triggered")
viewModel.onLocationUpdate = { location in
XCTAssertEqual(location.coordinate.latitude, 37.7749)
XCTAssertEqual(location.coordinate.longitude, -122.4194)
expectation.fulfill()
}
viewModel.fetchLocation()
wait(for: [expectation], timeout: 1.0)
}
}
Conclusion
• Dependency Injection makes code modular, reusable, and testable.
• DI in Swift allows easy substitution of services for testing.
• Using DI in MapKit, we separated concerns: the MapViewController focuses on UI, while MapViewModel handles logic.
This approach simplifies testing and code maintenance, making it a best practice for building scalable apps.
Top comments (0)