DEV Community

Anis Ali Khan
Anis Ali Khan

Posted on

Tutorial 20: Dependency Injection Tutorial

Table of Contents

  1. Introduction to Dependency Injection (DI)
  2. Why DI Matters: Testability & Reusability
  3. Types of Dependency Injection
  4. Implementing Dependency Injection in Swift (with MapKit Example)
  5. Unit Testing with Dependency Injection
  6. 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:

  1. Constructor Injection: Dependencies are passed via the initializer.
  2. Property Injection: Dependencies are assigned after object creation.
  3. 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

  1. Open Xcode → Create a new iOS App.
  2. Choose “App” and select Swift + UIKit.
  3. 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 }
}
Enter fullscreen mode Exit fullscreen mode

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)")
    }
}
Enter fullscreen mode Exit fullscreen mode

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()
    }
}
Enter fullscreen mode Exit fullscreen mode

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")
    }
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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()
    }
}
Enter fullscreen mode Exit fullscreen mode

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)
    }
}
Enter fullscreen mode Exit fullscreen mode

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)
    }
}
Enter fullscreen mode Exit fullscreen mode

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)