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.
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
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
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
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
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
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
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
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
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:
locations
propertyquery
propertyquerying
propertyAnd while we’re at it, we no longer need:
queryingDidChange
propertylocationsDidChange
propertyGreat. 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:
numberOfLocations
computed propertylocation(at:)
methodgeocode(addressString:)
methodAddLocationViewViewModel.swift
AddLocationViewViewModel.swift
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
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.