Before we use RxSwift and Combine to improve the current implementation of the Model-View-ViewModel pattern, I want to show you how you can roll your own solution using closures. I like to refer to this solution as DIY bindings or Do It Yourself bindings.
Creating the View Model
Let's write some code. Create a new group, View Models, in the Add Location View Controller group, and add a Swift file with name AddLocationViewModel.swift.

Define a class with name AddLocationViewModel. The implementation isn't too difficult, but there are several important details we need to pay attention to.
AddLocationViewModel.swift
import Foundation
class AddLocationViewModel {
}
Implementing the View Model
We import the Core Location framework and define a private, lazy, variable property, geocoder, of type CLGeocoder. The add location view controller no longer needs to know about the Core Location framework and it shouldn't perform the geocoding requests. This will be handled by the view model.
AddLocationViewModel.swift
import Foundation
import CoreLocation
class AddLocationViewModel {
// MARK: - Properties
private lazy var geocoder = CLGeocoder()
}
We also define a variable property, query, of type String. This property is updated by the view controller every time the user taps the search button or the cancel button.
AddLocationViewModel.swift
import Foundation
import CoreLocation
class AddLocationViewModel {
// MARK: - Properties
var query: String = ""
// MARK: -
private lazy var geocoder = CLGeocoder()
}
The view model needs to keep track of the locations and whether a geocoding request is in progress. We need to declare a few variables to manage state. We define a variable property of type Bool with name querying. The value of querying indicates whether a geocoding request is in progress. We also define a variable property of type [Location] with name locations. The locations property stores the result of the geocoding request.
AddLocationViewModel.swift
import Foundation
import CoreLocation
class AddLocationViewModel {
// MARK: - Properties
var query: String = ""
// MARK: -
private var querying = false
// MARK: -
private var locations: [Location] = []
// MARK: -
private lazy var geocoder = CLGeocoder()
}
Notice that both properties are defined privately. The view controller of the view model doesn't need to know about these properties and it certainly shouldn't be able to modify the values of these properties. We define several computed properties and methods the view controller can use to ask the view model for the data it needs to populate its view.
We define two computed properties, hasLocations of type Bool and numberOfLocations of type Int. The hasLocations computed property returns true if numberOfLocations is greater than 0. The numberOfLocations computed property returns the number of Location objects stored in the locations property.
AddLocationViewModel.swift
import Foundation
import CoreLocation
class AddLocationViewModel {
// MARK: - Properties
var query: String = ""
// MARK: -
private var querying = false
// MARK: -
private var locations: [Location] = []
var hasLocations: Bool {
numberOfLocations > 0
}
var numberOfLocations: Int {
locations.count
}
// MARK: -
private lazy var geocoder = CLGeocoder()
}
We also define two convenience methods to make the integration with the view controller straightforward. The first method, location(at:), returns the location for a given index. The method returns nil if the value of index is greater than or equal to the number of locations stored in the locations property.
AddLocationViewModel.swift
// MARK: - Public API
func location(at index: Int) -> Location? {
guard index < locations.count else {
return nil
}
return locations[index]
}
The second method, viewModelForLocation(at:), takes it a step further and returns an object of type LocationPresentable?. It uses the location(at:) method to fetch the Location object that corresponds with the value of index. The Location object is used to create a LocationViewModel object.
AddLocationViewModel.swift
func viewModelForLocation(at index: Int) -> LocationRepresentable? {
guard let location = location(at: index) else {
return nil
}
return LocationViewModel(location: location.location, locationAsString: location.name)
}
These computed properties and methods will make the implementations of the UITableViewDataSource and UITableViewDelegate protocols in the AddLocationViewController class straightforward.
Performing Geocoding Requests
Every time the value of the query property changes, the view model should perform a geocoding request. We can accomplish this using key-value-observing or by implementing a property observer. Let's keep it simple and implement a property observer.
In the property observer, we invoke the geocode(addressString:) method, passing in the value of the query property.
AddLocationViewModel.swift
var query: String = "" {
didSet {
geocode(addressString: query)
}
}
The implementation of the geocode(addressString:) method is straightforward. We use a guard statement to make sure addressString isn't equal to nil and the string stored in addressString isn't empty. We set the locations property to an empty array if either of these conditions isn't met.
AddLocationViewModel.swift
// MARK: - Helper Methods
private func geocode(addressString: String?) {
guard let addressString = addressString, !addressString.isEmpty else {
// Reset Location
locations = []
return
}
}
If addressString has a valid value, we set querying to true to indicate a geocoding request is in progress. We then invoke geocodeAddressString(_:completionHandler:) on the CLGeocoder instance.
AddLocationViewModel.swift
// MARK: - Helper Methods
private func geocode(addressString: String?) {
guard let addressString = addressString, !addressString.isEmpty else {
// Reset Location
locations = []
return
}
// Update Helper
querying = true
// Geocode Address String
geocoder.geocodeAddressString(addressString) { [weak self] (placemarks, error) in
}
}
In the completion handler, we define a helper variable, locations, of type [Location] and we set querying to false to indicate the geocoding request completed, successfully or unsuccessfully. If an error was thrown, the error is printed to the console.
AddLocationViewModel.swift
// MARK: - Helper Methods
private func geocode(addressString: String?) {
guard let addressString = addressString, !addressString.isEmpty else {
// Reset Location
locations = []
return
}
// Update Helper
querying = true
// Geocode Address String
geocoder.geocodeAddressString(addressString) { [weak self] (placemarks, error) in
// Create Buffer
var locations: [Location] = []
// Update Helper
self?.querying = false
if let error = error {
print("Unable to Forward Geocode Address (\(error))")
} else {
}
}
}
If an array with CLPlacemark instances is returned, we use compactMap(_:) to convert the placemarks to Location objects. We only create a Location object if the placemark has a name and a set of coordinates. The name and coordinates of the placemark are used to create a Location object.
Before we return from the completion handler, the locations property is updated with the value of the locations variable.
AddLocationViewModel.swift
// MARK: - Helper Methods
private func geocode(addressString: String?) {
guard let addressString = addressString, !addressString.isEmpty else {
// Reset Location
locations = []
return
}
// Update Helper
querying = true
// Geocode Address String
geocoder.geocodeAddressString(addressString) { [weak self] (placemarks, error) in
// Create Buffer
var locations: [Location] = []
// Update Helper
self?.querying = false
if let error = error {
print("Unable to Forward Geocode Address (\(error))")
} else if let placemarks = placemarks {
locations = placemarks.compactMap { (placemark) -> Location? in
guard let name = placemark.name else { return nil }
guard let location = placemark.location else { return nil }
return Location(name: name, latitude: location.coordinate.latitude, longitude: location.coordinate.longitude)
}
}
// Update Locations
self?.locations = locations
}
}
Notifying the View Controller
You may be wondering how the view controller is notified every time the value of querying or locations changes. That is where closures come into play. We need to define two more variable properties. Both properties are closures.
The first property, queryingDidChange, is a closure that accepts a boolean as its only argument. The second property, locationsDidChange, is a closure that accepts an array of Location objects as its only argument. Both properties are of an optional type.
AddLocationViewModel.swift
// MARK: -
var queryingDidChange: ((Bool) -> ())?
var locationsDidChange: (([Location]) -> ())?
The last piece of the puzzle is executing the closures at the appropriate times. Let me show you how this works. The view controller should be notified every time the value of querying or locations changes.
We have a few options. We can use key-value-observing or, as we did earlier, implement a property observer. Even though the latter is the easiest, I hope you can see that this isn't a scalable solution for large or complex applications.
Every time the querying property is set, the closure stored in queryingDidChange is executed and the value of the querying property is passed in as an argument.
AddLocationViewModel.swift
private var querying: Bool = false {
didSet {
queryingDidChange?(querying)
}
}
We implement a similar property observer for the locations property. Every time the locations property is set, the closure stored in locationsDidChange is executed and the value of the locations property is passed in as an argument.
AddLocationViewModel.swift
private var locations: [Location] = [] {
didSet {
locationsDidChange?(locations)
}
}
Time to Refactor
It is time to refactor the AddLocationViewController class. The solution we implemented in this episode is incomplete. The add location view controller is only notified if it sets the queryingDidChange and locationsDidChange properties. We take care of that in the next episode.