We put the foundation in place in the previous episode by refactoring the AddLocationViewModel class. We complete the integration with the Combine framework in this episode by refactoring the AddLocationViewController class.
Refactoring the View Controller
Open AddLocationViewController.swift and add an import statement for the Combine framework at the top.
import UIKit
import Combine
protocol AddLocationViewControllerDelegate: AnyObject {
func controller(_ controller: AddLocationViewController, didAddLocation location: Location)
}
class AddLocationViewController: UIViewController {
...
}
Navigate to the viewDidLoad() method and remove the references to the queryingDidChange and locationsDidChange properties of the AddLocationViewModel class.
// MARK: - View Life Cycle
override func viewDidLoad() {
super.viewDidLoad()
// Set Title
title = "Add Location"
// Initialize View Model
viewModel = AddLocationViewModel()
}
The view controller needs to subscribe to the locationsPublisher property and to the publisher of the querying property. Let's start with the locationsPublisher property. We attach a subscriber to the locationsPublisher property by invoking the sink(receiveValue:) method, passing in a closure. The view controller reloads its table view every time the locationsPublisher property emits a new value.
// MARK: - View Life Cycle
override func viewDidLoad() {
super.viewDidLoad()
// Set Title
title = "Add Location"
// Initialize View Model
viewModel = AddLocationViewModel()
// Subscribe to Locations Publisher
viewModel.locationsPublisher
.sink(receiveValue: { _ in
self.tableView.reloadData()
})
}
We need to take care of two issues. First, we don't want to strongly reference self in the closure we pass to the sink(receiveValue:) method. We use a capture list to weakly reference self and use optional chaining to safely access the view controller in the closure we pass to the sink(receiveValue:) method.
// MARK: - View Life Cycle
override func viewDidLoad() {
super.viewDidLoad()
// Set Title
title = "Add Location"
// Initialize View Model
viewModel = AddLocationViewModel()
// Subscribe to Locations Publisher
viewModel.locationsPublisher
.sink(receiveValue: { [weak self] _ in
self?.tableView.reloadData()
})
}
Second, remember from the previous episode that the sink(receiveValue:) method returns an AnyCancellable instance. We need to cancel the subscription when the add location view controller is deallocated. We implement the same pattern we implemented in the AddLocationViewModel class. We define a private, variable property with name subscriptions of type Set<AnyCancellable>. We set the initial value to an empty set.
private var subscriptions: Set<AnyCancellable> = []
To add the AnyCancellable instance returned by the sink(receiveValue:) method to the set of subscriptions, we invoke the store(in:) method on the AnyCancellable instance, passing in the set of subscriptions as an in-out parameter. We covered this in the previous episode.
// MARK: - View Life Cycle
override func viewDidLoad() {
super.viewDidLoad()
// Set Title
title = "Add Location"
// Initialize View Model
viewModel = AddLocationViewModel()
// Subscribe to Locations Publisher
viewModel.locationsPublisher
.sink(receiveValue: { [weak self] _ in
self?.tableView.reloadData()
}).store(in: &subscriptions)
}
To show the activity indicator view when a geocoding request is in progress, we attach a subscriber to the querying publisher. This is similar to attaching a subscriber to the locationsPublisher property. In the closure we pass to the sink(receiveValue:) method, we start animating the activity indicator view if the published value is true and we stop animating the activity indicator view if the published value is false. We store the subscriber returned by the sink(receiveValue:) method in the set of subscriptions.
// MARK: - View Life Cycle
override func viewDidLoad() {
super.viewDidLoad()
// Set Title
title = "Add Location"
// Initialize View Model
viewModel = AddLocationViewModel()
// Subscribe to Locations Publisher
viewModel.locationsPublisher
.sink(receiveValue: { [weak self] _ in
self?.tableView.reloadData()
}).store(in: &subscriptions)
// Subscribe to Querying
viewModel.$querying
.sink(receiveValue: { [weak self] (isQuerying) in
if isQuerying {
self?.activityIndicatorView.startAnimating()
} else {
self?.activityIndicatorView.stopAnimating()
}
}).store(in: &subscriptions)
}
Updating the Search Bar Delegate Implementation
The view controller is notified when the array of locations changes and when a geocoding request is in progress. It is the responsibility of the view controller to notify the view model when the text of the search bar changes.
The good news is that the view controller already notifies the view model when the user taps the search button or the cancel button. To complete the integration, we need to implement two additional methods of the UISearchBarDelegate protocol.
First, we implement the searchBar(_:textDidChange:) method. Every time the text of the search bar changes, the view controller updates the query property of the view model.
extension AddLocationViewController: UISearchBarDelegate {
func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
// Update Query
viewModel.query = searchText
}
...
}
Second, we implement the searchBarTextDidEndEditing(_:) method. The view controller asks the search bar for the value of its text property and assigns it to the view model's query property. If the text property doesn't have a value, we default to an empty string.
extension AddLocationViewController: UISearchBarDelegate {
func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
// Update Query
viewModel.query = searchText
}
func searchBarTextDidEndEditing(_ searchBar: UISearchBar) {
// Update Query
viewModel.query = searchBar.text ?? ""
}
...
}
Run the application and perform a search to make sure everything is working as expected.
What's Next?
Even though the learning curve of the Combine framework is less steep than that of RxSwift and RxCocoa, it is important to know that RxSwift and RxCocoa have a broader set of features and have been around for much longer. The implementation of RxSwift and RxCocoa is cleaner and more elegant. The add location view controller passes a driver to the initializer of the AddLocationViewModel class. There is no need to manually update the query property of the view model.
The bindings RxSwift and RxCocoa offer are more advanced. The view model drives the user interface of the view controller's view. There is no need to explicitly subscribe to streams. My hope is that Apple continues to invest in Combine to make it more expressive and less verbose.