22 Refactoring the View Model

This chapter hasn’t been updated for Xcode 9 and Swift 4. This chapter will be updated as soon as RxSwift/RxCocoa officially support Swift 4.

Refactoring the View Model

Make sure you open the workspace CocoaPods created for us in the previous chapter. Open AddLocationViewViewModel.swift and add an import statement for RxSwift and RxCocoa at the top.

AddLocationViewViewModel.swift

 1 import RxSwift
 2 import RxCocoa
 3 import Foundation
 4 import CoreLocation
 5 
 6 class AddLocationViewViewModel {
 7 
 8     ...
 9 
10 }

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. But we can do better. 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 AddLocationViewViewModel class. The initializer accepts one argument, a driver of type String. Don’t worry if you’re 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’s all you need to know about drivers to follow along.

AddLocationViewViewModel.swift

1 // MARK: - Initializtion
2 
3 init(query: Driver<String>) {
4 
5 }

Since we’re using RxSwift and RxCocoa, it’d be crazy not to take advantage of the other features these libraries 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.

AddLocationViewViewModel.swift

1 // MARK: - Initializtion
2 
3 init(query: Driver<String>) {
4     query
5         .throttle(0.5)
6         .distinctUntilChanged()
7 }

We subscribe to the sequence of values by invoking the drive(onNext:onCompleted:onDisposed:) method on the sequence. The onNext handler is invoked when a new value is emitted by the sequence. This happens when the user modifies the text in the search bar.

AddLocationViewViewModel.swift

 1 // MARK: - Initializtion
 2 
 3 init(query: Driver<String>) {
 4     query
 5         .throttle(0.5)
 6         .distinctUntilChanged()
 7         .drive(onNext: { [weak self] (addressString) in
 8             self?.geocode(addressString: addressString)
 9         })
10         .disposed(by: disposeBag)
11 }

When a new value is emitted by the sequence, we send a geocoding request by invoking the geocode(addressString:) method, which we implemented earlier in this book. You don’t need to worry about the disposed(by:) method call. It is related to memory management and is specific to RxSwift. To make this work, we need to define the disposeBag property in the AddLocationViewViewModel class.

AddLocationViewViewModel.swift

1 // MARK: -
2 
3 private let disposeBag = DisposeBag()

Reducing State

The current implementation of the AddLocationViewViewModel 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 keeps the better. This is another advantage of reactive programming. Let me show you what I mean.

We can improve this with RxSwift and RxCocoa by keeping a reference to the stream of results of the geocoding requests. The result is that 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 several details that need our attention. We declare a constant, private property _locations of type Variable. The Variable is of type [Location]. You can think of a Variable as the pipeline and [Location] as the data that flows through that pipeline. We initialize the pipeline with an empty array of locations.

AddLocationViewViewModel.swift

1 private let _locations = Variable<[Location]>([])

We can do the same for the querying property. We declare a constant, private property, _querying, of type Variable. The Variable is of type Bool. This means that the values that flow through the pipeline are boolean values.

AddLocationViewViewModel.swift

1 private let _querying = Variable<Bool>(false)

There’s a good reason for declaring these properties private. What we expose to the view controller are drivers. What’s the difference between drivers and variables? To keep it simple, think of drivers as read-only and variables 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 querying and locations.

AddLocationViewViewModel.swift

1 var querying: Driver<Bool> { return _querying.asDriver() }
2 var locations: Driver<[Location]> { return _locations.asDriver() }

The syntax may look daunting, but it really isn’t. querying is a computed property of type Driver. The driver is of type Bool. The implementation is simple. We return the Variable _querying as a driver. The same is true for locations. locations is a computed property of type Driver. The driver is of type [Location]. We return the Variable _locations as a driver.

We expose two computed properties and we simply return the private variables as drivers. Are you still with me?

Let’s clean up the pieces we no longer need. We can remove a few properties:

And while we’re at it, we no longer need:

Great. 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 Variable exposes its current value through its value property. This means we need to update:

AddLocationViewViewModel.swift

1 var numberOfLocations: Int { return _locations.value.count }

AddLocationViewViewModel.swift

1 func location(at index: Int) -> Location? {
2     guard index < _locations.value.count else { return nil }
3     return _locations.value[index]
4 }

In geocode(addressString:), we also need to replace querying with _querying.value. We access the current value of the _querying reactive Variable through its value property.

AddLocationViewViewModel.swift

 1 private func geocode(addressString: String?) {
 2     guard let addressString = addressString, !addressString.isEmpty \
 3 else {
 4         _locations.value = []
 5         return
 6     }
 7 
 8     _querying.value = true
 9 
10     // Geocode Address String
11     geocoder.geocodeAddressString(addressString) { [weak self] (plac\
12 emarks, error) in
13         var locations: [Location] = []
14 
15         self?._querying.value = false
16 
17         if let error = error {
18             print("Unable to Forward Geocode Address (\(error))")
19 
20         } else if let _placemarks = placemarks {
21             locations = _placemarks.flatMap({ (placemark) -> Locatio\
22 n? in
23                 guard let name = placemark.name else { return nil }
24                 guard let location = placemark.location else { retur\
25 n nil }
26                 return Location(name: name, latitude: location.coord\
27 inate.latitude, longitude: location.coordinate.longitude)
28             })
29         }
30 
31         self?._locations.value = locations
32     }
33 }

That looks good. We can’t run the application yet because we’ve made some breaking changes. We need to make a few modifications to the AddLocationViewController class.