CoreLocation APIs provide the Location updates in iOS. We need to define a protocol around the CoreLocation features to make it easy to mock, and inject as dependency. Let’s name the protocol as LocationProvidable, which that provides location updates to the ViewModel using the platform API.

protocol LocationProvidable {
    var listener: LocationObservable? { get set }
    func setListener(listener: LocationObservable)
    func startLocationUpdates()
    func getCurrentLocation() -> (Double, Double)
}
  • The startLocationUpdates() method will be used to start the location updates.
  • getCurrentLocation() will be used to get the current location as a Tuple.

The LocationProvidable relays it’s updates through its delegate LocationObservable, which is injected through the method setListener().

The Delegate Protocol LocationObservable is defined as:

protocol LocationObservable {
    func setCurrentLocation(latitude: Double, longitude: Double)
}

The standard way to listen to CoreLocation updates is to use a CLLocationManager instance. Keeping in mind our objective of loose coupling, and dependency injection, we need to define a protocol to wrap the LocationManager. We’ll inject it’s as a dependency, and mock it for unit testing.

The protocol LocationManagerConfigurable is just a wrapper around the CLLocationManager

protocol LocationManagerConfigurable {
    // wrap `delegate` and `desiredAccuracy` to make it decoupled
    func setDelegate(to instance: AnyObject)
    func setDesiredAccuracy(to accuracy: Double)
    func requestAlwaysAuthorization()
    func startUpdatingLocation()
}

Finally we’ll extend the CLLocationManager to adhere to the new protocol.

import CoreLocation

// CoreLocation extenstion for protocol conformance
extension CLLocationManager: LocationManagerConfigurable {
    func setDelegate(to instance: AnyObject) {
        guard let delegate = instance as? CLLocationManagerDelegate else {
            return
        }
        self.delegate = delegate
    }
    func setDesiredAccuracy(to accuracy: Double) {
        let accuracy = accuracy as CLLocationAccuracy
        self.desiredAccuracy = accuracy
    }
}

Now we’re free to use the protocol LocationManagerConfigurable, and choose the implementation based on our needs.

Class Diagrams

The class diagram shows the relationship between the elements.

Class Diagram

Implementation

We’re now ready to implement the LocationProvidable protocol. We’ll need a reference to the current CLLocation, and LocationManagerConfigurable instance. The dependency locationManager is injected through the constructor.

init(locationManager:LocationManagerConfigurable){
        self.locationManager = locationManager        
    }

We need to implement locationManager(_:didUpdateLocations:) through which CoreLocation relays the location updates:

func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
    guard let lastLocation = locations.last,
        let listener = self.listener else {
            return
    }
    
    if currentLocation != nil && Double((currentLocation?.distance(from: lastLocation))!) < 100.0 {
        //print("current Location \(String(describing: currentLocation?.coordinate)) is same as last Location: \(String(describing: lastLocation.coordinate))")
        return
    }
    
    currentLocation = lastLocation
    listener.setCurrentLocation(latitude: lastLocation.coordinate.latitude, longitude: lastLocation.coordinate.longitude)
}

Unit tests

let’s write some unit tests to test the loosely coupled architecture for the location component.

Mocks for the unit tests

Class MockLocationProvider

import MapKit

class MockLocationProvider: LocationProvidable {
    var listener: LocationObservable?
    
    func setListener(listener: LocationObservable) {
        self.listener = listener
    }
    
    internal var callCount = 0
    func startLocationUpdates() {
        callCount += 1
        listener?.setCurrentLocation(latitude: 52.51631, longitude: 13.37777)
    }
    
    func getCurrentLocation() -> (Double, Double) {
        callCount += 1
        return (52.51631, 13.37777)
    }
}

Class MockLocationObservable

class MockLocationObservable: LocationObservable {
    internal var coordinates: (Double, Double)?
    func setCurrentLocation(latitude: Double, longitude: Double) {
        coordinates = (latitude, longitude)
    }
}

Class MockLocationManager

class MockLocationManager: LocationManagerConfigurable {
    internal var callCount = 0
    fileprivate var delegate: LocationProvider?
    
    func setDelegate(to instance: AnyObject) {
        callCount += 1
        delegate = instance as? LocationProvider
    }
    
    func setDesiredAccuracy(to accuracy: Double) {
        callCount += 1
    }
    
    func requestAlwaysAuthorization() {
        callCount += 1
    }
    func startUpdatingLocation() {
        callCount += 1
        updateLocation()
    }
    func updateLocation() {
        let mockLocation = CLLocation(latitude: CLLocationDegrees(52.51631), longitude: CLLocationDegrees(13.37777))
        let mockLocationManager = CLLocationManager()
        delegate?.locationManager(mockLocationManager, didUpdateLocations: [mockLocation])
    }
}

The Unit Tests are written in BDD style, and we inject the Mocked dependencies to our test target LocationProvider.

import XCTest
import Quick
import Nimble

class LocationProviderTests: QuickSpec {
    override func spec() {
        describe("Given a LocationProvider") {
            context("When it's started with LocationManager", closure: {
                // Arrange
                let mockLocationManager = MockLocationManager()
                let mockLocationObservable = MockLocationObservable()
                let locationProvider: LocationProvidable = LocationProvider(locationManager: mockLocationManager)
                beforeEach {
                    mockLocationManager.callCount = 0
                }
                it("then starts location updates", closure: {
                    locationProvider.setListener(listener: mockLocationObservable)
                    // Act
                    locationProvider.startLocationUpdates()
                    //Assert
                    expect(mockLocationManager.callCount).toEventually(equal(4))
                    expect(mockLocationObservable.coordinates).toEventuallyNot(beNil())
                    expect(mockLocationObservable.coordinates?.0).toEventually(equal(52.51631))
                    expect(mockLocationObservable.coordinates?.1).toEventually(equal(13.37777))
                })
                
                it("then provides current location", closure: {
                    // Act
                    let (lat, lon) = locationProvider.getCurrentLocation()
                    //Assert
                    expect(lat).to(equal(52.51631))
                    expect(lon).to(equal(13.37777))
                })
            })
        }
    }
}

Running the tests should give the following output:

Test Suite 'LocationProviderTests' started at 2018-03-04 20:41:07.475
Test Case '-[LooLocatorTests.LocationProviderTests Given_a_LocationProvider__When_it_s_started_with_LocationManager__then_starts_location_updates]' started.
Test Case '-[LooLocatorTests.LocationProviderTests Given_a_LocationProvider__When_it_s_started_with_LocationManager__then_starts_location_updates]' passed (0.015 seconds).
Test Case '-[LooLocatorTests.LocationProviderTests Given_a_LocationProvider__When_it_s_started_with_LocationManager__then_provides_current_location]' started.
Test Case '-[LooLocatorTests.LocationProviderTests Given_a_LocationProvider__When_it_s_started_with_LocationManager__then_provides_current_location]' passed (0.001 seconds).
Test Suite 'LocationProviderTests' passed at 2018-03-04 20:41:07.492.
	 Executed 2 tests, with 0 failures (0 unexpected) in 0.016 (0.017) seconds

Voila! Now we’ve a modular and tested Location component.

In the next post we’ll see how to design the modular networking component.