23 Refactoring the View Controller

Open AddLocationViewController.swift and add an import statement for RxSwift and RxCocoa at the top.

AddLocationViewController.swift

 1 import UIKit
 2 import RxSwift
 3 import RxCocoa
 4 
 5 protocol AddLocationViewControllerDelegate {
 6     func controller(_ controller: AddLocationViewController, didAddL\
 7 ocation location: Location)
 8 }
 9 
10 class AddLocationViewController: UIViewController {
11 
12     ...
13 
14 }

We also need to declare a property, disposeBag, of type DisposeBag. As I mentioned in the previous chapter, don’t worry about this if you’re not familiar with RxSwift. The goal is to learn how the Model-View-ViewModel pattern works with bindings. We’re not here to learn RxSwift.

AddLocationViewController.swift

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

Our next stop is the viewDidLoad() method of the AddLocationViewController class. We need to update the initializer of the AddLocationViewViewModel class. We pass a driver as the only argument of the initializer. Because we imported RxCocoa, we have access to the reactive extensions of UISearchBar.

AddLocationViewController.swift

 1 // MARK: - View Life Cycle
 2 
 3 override func viewDidLoad() {
 4     super.viewDidLoad()
 5 
 6     // Set Title
 7     title = "Add Location"
 8 
 9     // Initialize View Model
10     viewModel = AddLocationViewViewModel(query: searchBar.rx.text.or\
11 Empty.asDriver())
12 
13     // Configure View Model
14     viewModel.locationsDidChange = { [unowned self] (locations) in
15         self.tableView.reloadData()
16     }
17 
18     viewModel.queryingDidChange = { [unowned self] (querying) in
19         if querying {
20             self.activityIndicatorView.startAnimating()
21         } else {
22             self.activityIndicatorView.stopAnimating()
23         }
24     }
25 }

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 AddLocationViewViewModel class.

AddLocationViewController.swift

1 // Initialize View Model
2 viewModel = AddLocationViewViewModel(query: searchBar.rx.text.orEmpt\
3 y.asDriver())

We can remove the remaining lines from the viewDidLoad() method. Instead, we’re going to use bindings to update the user interface if the view model performs a geocoding request and when it receives a response.

AddLocationViewController.swift

 1 // MARK: - View Life Cycle
 2 
 3 override func viewDidLoad() {
 4     super.viewDidLoad()
 5 
 6     // Set Title
 7     title = "Add Location"
 8 
 9     // Initialize View Model
10     viewModel = AddLocationViewViewModel(query: searchBar.rx.text.or\
11 Empty.asDriver())
12 }

We listen for events of the locations driver of the view model. If a new event is emitted, the table view is reloaded. Because the view model is owned by the view controller, we use an unowned reference to self within the closure.

AddLocationViewController.swift

1 // Drive Table View
2 viewModel.locations.drive(onNext: { [unowned self] (_) in
3         // Update Table View
4         self.tableView.reloadData()
5     })
6     .disposed(by: disposeBag)

To show you how powerful and elegant Rx is, we use the querying driver of the view model to start and stop animating the activity indicator view.

AddLocationViewController.swift

1 // Drive Activity Indicator View
2 viewModel.querying.drive(activityIndicatorView.rx.isAnimating).addDi\
3 sposableTo(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

 1 searchBar.rx.searchButtonClicked
 2     .asDriver(onErrorJustReturn: ())
 3     .drive(onNext: { [unowned self] in
 4         self.searchBar.resignFirstResponder()
 5     })
 6     .disposed(by: disposeBag)
 7 
 8 searchBar.rx.cancelButtonClicked
 9     .asDriver(onErrorJustReturn: ())
10     .drive(onNext: { [unowned self] in
11         self.searchBar.resignFirstResponder()
12     })
13     .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.

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 AddLocationViewViewModel class in the AddLocationViewController 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’s our first accomplishment.

But, more importantly, the view controller no longer manages state. This is thanks to Rx and the Model-View-ViewModel pattern. The less state your application manages the better and this is especially true for view controllers. But what has changed?

User input is funneled to the view model.
User input is funneled to the view model.

The user’s input is directly funneled to 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 locations 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 that indicate whether a geocoding request is in flight. If you’re 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’s a small win but nevertheless welcome.

We’re not quite done yet. In this book, I promised you that testing becomes easier if you adopt the Model-View-ViewModel pattern. Let’s put that to the test like we did earlier in this book. But, first, we need to deal with an obstacle that’s preventing us from writing good unit tests.