LooLocator: The Location provider
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.
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.