With RxSwift and RxCocoa integrated into the project, it is time to refactor the AddLocationViewModel class. Make sure you open the workspace CocoaPods created for us in the previous episode. Open AddLocationViewModel.swift and add an import statement for RxSwift and RxCocoa at the top.
AddLocationViewModel.swift
import RxSwift
import RxCocoa
import Foundation
import CoreLocation
class AddLocationViewModel {
...
}
The search bar of the add location view controller drives the view model. At the moment, the view controller sets the value of the query property every time the text of the search bar changes. By taking advantage of reactive programming, we can improve the implementation. We can replace the query property and pass a driver of type String to the initializer of the view model. Let me show you how this works.
We define an initializer for the AddLocationViewModel class. The initializer accepts one argument, a driver of type String. Don't worry if you are not familiar with drivers. Think of a driver as a stream or sequence of values. Instead of having a property, query, with a value, a driver is a stream or sequence of values other objects can subscribe to. That is all you need to know about drivers to follow along.
AddLocationViewModel.swift
// MARK: - Initialization
init(query: Driver<String>) {
}
Since we are using RxSwift and RxCocoa, it would be crazy not to take advantage of the other features these libraries have to offer. In the initializer, we apply two operators to the query driver. We apply the throttle(_:) operator, to limit the number of requests that are sent in a period of time, and the distinctUntilChanged() operator, to prevent sending geocoding requests to Apple's location services for the same query.
AddLocationViewModel.swift
// MARK: - Initialization
init(query: Driver<String>) {
query
.throttle(.seconds(1))
.distinctUntilChanged()
}
The view model subscribes to the sequence of values by invoking the drive(onNext:onCompleted:onDisposed:) method on the sequence. The onNext handler is invoked every time the sequence emits a value. This happens when the user modifies the text in the search bar.
AddLocationViewModel.swift
// MARK: - Initialization
init(query: Driver<String>) {
query
.throttle(0.5)
.distinctUntilChanged()
.drive(onNext: { [weak self] (addressString) in
self?.geocode(addressString: addressString)
}).disposed(by: disposeBag)
}
Every time the sequence emits a value, the view model sends a geocoding request by invoking the geocode(addressString:) method, a helper method we implemented earlier in this series. Don't worry about the disposed(by:) method call. It relates to memory management and is specific to RxSwift. Before we continue, we need to define the disposeBag property in the AddLocationViewModel class.
AddLocationViewModel.swift
// MARK: -
private let disposeBag = DisposeBag()
Reducing State
The current implementation of the AddLocationViewModel class keeps a reference to the results of the geocoding requests. In other words, it manages state. While this isn't a problem, the fewer bits of state an object manages the better. This is another advantage of reactive programming. Let me show you what I mean.
We can improve the implementation using RxSwift and RxCocoa by keeping a reference to the stream of results of the geocoding requests. As a result, the view model no longer manages state. It simply holds a reference to the pipeline through which the results of the geocoding requests flow.
The change is small, but there are a few details we need to take care of. We declare a constant, private property locationsRelay of type BehaviorRelay. The behavior relay is of type [Location]. You can think of a behavior relay as the pipeline and [Location] as the data that flows through that pipeline. We initialize the pipeline with an empty array of Location objects.
AddLocationViewModel.swift
private let locationsRelay = BehaviorRelay<[Location]>(value: [])
We can do the same for the querying property. We declare a constant, private property, queryingRelay, of type BehaviorRelay. The behavior relay is of type Bool. This means that the values that flow through the pipeline are booleans.
AddLocationViewModel.swift
private let queryingRelay = BehaviorRelay<Bool>(value: false)
There is a good reason for declaring these properties privately. What we expose to the view controller are drivers. What is the difference between drivers and behavior relays? To keep it simple, think of drivers as read-only and behavior relays as read-write. We don't want the view controller to make changes to the stream of locations, for example. The drivers we expose to the view controller are queryingDriver and locationsDriver.
AddLocationViewModel.swift
// MARK: -
var locationsDriver: Driver<[Location]> {
locationsRelay.asDriver()
}
private let locationsRelay = BehaviorRelay<[Location]>(value: [])
// MARK: -
var queryingDriver: Driver<Bool> {
queryingRelay.asDriver()
}
private let queryingRelay = BehaviorRelay<Bool>(value: false)
The syntax may look daunting, but it really isn't. locationsDriver is a computed property of type Driver. The driver is of type [Location]. The implementation is simple. We return the behavior relay locationsRelay as a driver.
The same is true for queryingDriver. queryingDriver is a computed property of type Driver. The driver is of type Bool. We return the behavior relay queryingRelay as a driver.
We expose two computed properties and we simply return the private behavior relays as drivers. Let's clean up the pieces we no longer need. We can remove a few properties:
- the
locationsproperty - the
queryproperty - the
queryingproperty
And while we are at it, we no longer need:
- the
queryingDidChangeproperty - the
locationsDidChangeproperty
The last thing we need to do is make a few changes to how the view model accesses the array of locations. The changes are minor. A reactive behavior relay exposes its current value through its value property. This means we need to update:
- the
numberOfLocationscomputed property - the
location(at:)method - the
geocode(addressString:)method
AddLocationViewModel.swift
var numberOfLocations: Int {
locationsRelay.value.count
}
AddLocationViewModel.swift
func location(at index: Int) -> Location? {
guard index < locationsRelay.value.count else {
return nil
}
return locationsRelay.value[index]
}
In geocode(addressString:), we also need to replace querying with queryingRelay and locations with locationsRelay. We access the current value of the queryingRelay and locationsRelay behavior relays through their value property and we emit a new value by passing the new value to their accept(_:) method.
AddLocationViewModel.swift
private func geocode(addressString: String?) {
guard let addressString = addressString, !addressString.isEmpty else {
// Reset Locations
locationsRelay.accept([])
return
}
// Update Helper
queryingRelay.accept(true)
// Geocode Address String
geocoder.geocodeAddressString(addressString) { [weak self] (placemarks, error) in
// Create Buffer
var locations: [Location] = []
// Update Helper
self?.queryingRelay.accept(false)
if let error = error {
print("Unable to Forward Geocode Address (\(error))")
} else if let placemarks = placemarks {
locations = placemarks.compactMap { (placemark) -> Location? in
guard let name = placemark.name else { return nil }
guard let location = placemark.location else { return nil }
return Location(name: name, latitude: location.coordinate.latitude, longitude: location.coordinate.longitude)
}
}
// Update Locations
self?.locationsRelay.accept(locations)
}
}
That's a good start. We are unable to run the application at the moment because we have made some breaking changes. We need to make a few changes to the implementation of the AddLocationViewController class. We take care of that in the next episode.