The changes we made in the previous episode broke the application. To fix what we broke, we need to update the implementation of the AddLocationViewController class. Open AddLocationViewController.swift and add an import statement for RxSwift and RxCocoa at the top.
AddLocationViewController.swift
import UIKit
import RxSwift
import RxCocoa
protocol AddLocationViewControllerDelegate: AnyObject {
func controller(_ controller: AddLocationViewController, didAddLocation location: Location)
}
class AddLocationViewController: UIViewController {
...
}
We also need to declare a property, disposeBag, of type DisposeBag. As I mentioned in the previous episode, don't worry about this implementation detail if you are not familiar with RxSwift. The goal is to learn how the Model-View-ViewModel pattern can be improved through bindings. We won't be focusing on RxSwift.
AddLocationViewController.swift
// MARK: -
private let disposeBag = DisposeBag()
Our next stop is the viewDidLoad() method of the AddLocationViewController class. We need to modify the initialization of the AddLocationViewModel instance. The add location view controller passes a driver to the initializer of the AddLocationViewModel class. We could also say that the add location view controller injects a driver into the add location view model.
Because we imported RxCocoa, we have access to the reactive extensions of the UISearchBar class. A search bar emits a sequence of String values. We ask it for a reference to that sequence. The orEmpty operator converts any nil values to an empty string. The asDriver() method turns the sequence into a driver. We pass this driver of type String to the initializer of the AddLocationViewModel class.
AddLocationViewController.swift
// MARK: - View Life Cycle
override func viewDidLoad() {
super.viewDidLoad()
// Set Title
title = "Add Location"
// Initialize View Model
viewModel = AddLocationViewModel(query: searchBar.rx.text.orEmpty.asDriver())
// Configure View Model
viewModel.locationsDidChange = { [weak self] (locations) in
self?.tableView.reloadData()
}
viewModel.queryingDidChange = { [weak self] (querying) in
if querying {
self?.activityIndicatorView.startAnimating()
} else {
self?.activityIndicatorView.stopAnimating()
}
}
}
We can remove the remaining lines from the viewDidLoad() method. Instead, we use bindings to update the user interface if the view model performs a geocoding request and when it receives a response.
AddLocationViewController.swift
// MARK: - View Life Cycle
override func viewDidLoad() {
super.viewDidLoad()
// Set Title
title = "Add Location"
// Initialize View Model
viewModel = AddLocationViewModel(query: searchBar.rx.text.orEmpty.asDriver())
}
The add location view controller listens for events of the locationsDriver driver of the view model. Every time locationsDriver emits an event, the add location view controller reloads its table view.
AddLocationViewController.swift
// Drive Table View
viewModel?.locationsDriver
.drive(onNext: { [weak self] (_) in
// Update Table View
self?.tableView.reloadData()
})
.disposed(by: disposeBag)
To show you how powerful and elegant Rx is, we use the queryingDriver driver of the view model to start and stop animating the activity indicator view.
AddLocationViewController.swift
// Drive Activity Indicator View
viewModel?.queryingDriver.drive(activityIndicatorView.rx.isAnimating).disposed(by: disposeBag)
We use a similar technique to hide the keyboard. When the user taps the search or cancel buttons, we resign the search bar as the first responder.
AddLocationViewController.swift
// Search Button Clicked
searchBar.rx.searchButtonClicked.asDriver()
.drive(onNext: { [weak self] in
self?.searchBar.resignFirstResponder()
})
.disposed(by: disposeBag)
// Cancel Button Clicked
searchBar.rx.cancelButtonClicked.asDriver()
.drive(onNext: { [weak self] in
self?.searchBar.resignFirstResponder()
})
.disposed(by: disposeBag)
This means we can remove the implementation of the UISearchBarDelegate protocol in its entirety. Delegation is a nice pattern, but it feels great every time I can use Rx to replace boilerplate code like this.
Make sure that you only remove the implementation of the UISearchBarDelegate protocol methods. Leave the extension for the UISearchBarDelegate protocol untouched because the AddLocationViewController class is required to conform to the UISearchBarDelegate protocol.
AddLocationViewController.swift
extension AddLocationViewController: UISearchBarDelegate {
}
We could do the same for the UITableViewDataSource and UITableViewDelegate protocols, but I don't want to overwhelm you too much at this point. Build and run Cloudy to make sure we didn't break anything during the refactoring operation.
What Have We Accomplished
You may be wondering what we gained by introducing the Model-View-ViewModel pattern and the AddLocationViewModel class. Let's take a look.
The view controller is no longer in charge of forward geocoding. In fact, it doesn't even know about the Core Location framework. That is an important accomplishment.
But, more important, the view controller no longer manages state thanks to Rx and the Model-View-ViewModel pattern. The less state an application manages the better and this is especially true for view controllers. But what has changed?
The user's input is forwarded the view model. The view model uses the input of the search bar to perform geocoding requests. The results of these geocoding requests are streamed back to the view controller through the locationsDriver driver and the view controller's table view is updated as a result.
The view model doesn't keep any state either. In true Rx fashion, it manages two data streams, a stream of arrays with locations and a stream of boolean values to indicate whether a geocoding request is in flight. If you are new to reactive programming and bindings, then this may take some getting used to. But I hope you agree that the result is a welcome improvement.
We also got rid of the UISearchBarDelegate protocol implementation. It is a small win but nevertheless welcome.
What's Next?
While I enjoy working with RxSwift and RxCocoa, Apple's Combine framework is a promising alternative. In the next episodes, we replace RxSwift and RxCocoa with Apple's Combine framework.