You should now have a good understanding of what the Model-View-ViewModel pattern is and how it can be used to cure some of the problems the Model-View-Controller pattern suffers from. We can do better, though. Data is currently flowing in one direction. The view controller asks its view model for data and populates the view it manages. This is fine and many projects can greatly benefit from this lightweight implementation of the Model-View-ViewModel pattern.

Data Flows in One Direction

But it is time to show you how we can take the Model-View-ViewModel pattern to the next level. In the next few episodes, we discuss how MVVM can be used to not only populate a view with data but also respond to changes made by the user or a change of the environment.

One of Cloudy's features is the ability to search for and save locations. When the user selects one of the saved locations, Cloudy fetches weather data for that location and displays it to the user.

Let me show you how this works. When the user taps the location button in the top left, the locations view is shown. It lists the current location of the device as well as the user's saved locations.

Cloudy can manage a list of locations.

To add a location to the list of saved locations, the user taps the plus button in the top left. This brings up the add location view.

The user can add locations using the add location view controller.

The user enters the name of a city and Cloudy uses the Core Location framework to forward geocode the location. Under the hood, Cloudy asks the Core Location framework for the coordinates of the city the user entered.

When the user enters the name of a city in the search bar and taps the Search button, the view controller uses a CLGeocoder instance to forward geocode the location. Forward geocoding is an asynchronous operation. The view controller updates its table view when the Core Location framework returns the results of the geocoding request.

The current implementation of this feature uses the Model-View-Controller pattern. But you want to know how we can improve this using the Model-View-ViewModel pattern. That is what you are here for.

How can we improve what we currently have? We will create a view model that is responsible for everything related to responding to the user's input. The view model will send the geocoding request to Apple's location services. This is an asynchronous operation. When the geocoding request completes, successfully or unsuccessfully, the view controller's table view is updated with the results of the geocoding request. This example will show you how powerful the Model-View-ViewModel pattern can be when it is correctly implemented.

Data Flows in Both Directions

Let's take a quick look at the current implementation, which uses the Model-View-Controller pattern. We are only focusing on the AddLocationViewController class. It defines outlets for a table view and a search bar. It also maintains a list of Location objects. These are the results the Core Location framework returns. The Location type is a struct that makes working with the results of the Core Location framework easier. The CLGeocoder instance is responsible for making the geocoding requests. Don't worry if you are not familiar with this class, it is very easy to use.

AddLocationViewController.swift

class AddLocationViewController: UIViewController {

    // MARK: - Properties

    @IBOutlet var tableView: UITableView!
    @IBOutlet var searchBar: UISearchBar!

    // MARK: -

    private var locations: [Location] = []

    // MARK: -

    private lazy var geocoder = CLGeocoder()

    // MARK: -

    weak var delegate: AddLocationViewControllerDelegate?

    ...

}

We also define a delegate protocol, AddLocationViewControllerDelegate, to notify the LocationsViewController class of the user's selection. This isn't important for the rest of the discussion, though.

AddLocationViewController.swift

protocol AddLocationViewControllerDelegate: AnyObject {
    func controller(_ controller: AddLocationViewController, didAddLocation location: Location)
}

The AddLocationViewController class acts as the delegate of the search bar and it conforms to the UISearchBarDelegate protocol. When the user taps the Search button, the text of the search bar is used as input for the geocoding request.

AddLocationViewController.swift

extension AddLocationViewController: UISearchBarDelegate {

    func searchBarSearchButtonClicked(_ searchBar: UISearchBar) {
        // Hide Keyboard
        searchBar.resignFirstResponder()

        // Forward Geocode Address String
        geocode(addressString: searchBar.text)
    }

    func searchBarCancelButtonClicked(_ searchBar: UISearchBar) {
        // Hide Keyboard
        searchBar.resignFirstResponder()

        // Clear Locations
        locations = []

        // Update Table View
        tableView.reloadData()
    }

}

Notice that we already use a dash of MVVM in the UITableViewDataSource protocol to populate the table view. We take it a few steps further in the next few episodes.

AddLocationViewController.swift

extension AddLocationViewController: UITableViewDataSource {

    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return locations.count
    }

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        guard let cell = tableView.dequeueReusableCell(withIdentifier: LocationTableViewCell.reuseIdentifier, for: indexPath) as? LocationTableViewCell else { fatalError("Unexpected Table View Cell") }

        // Fetch Location
        let location = locations[indexPath.row]

        // Create View Model
        let viewModel = LocationViewModel(location: location.location, locationAsString: location.name)

        // Configure Table View Cell
        cell.configure(withViewModel: viewModel)

        return cell
    }

}

The Core Location framework makes forward geocoding easy. The magic happens in the geocode(addressString:) method.

AddLocationViewController.swift

private func geocode(addressString: String?) {
    guard let addressString = addressString else {
        // Clear Locations
        locations = []

        // Update Table View
        tableView.reloadData()

        return
    }

    // Geocode City
    geocoder.geocodeAddressString(addressString) { [weak self] (placemarks, error) in
        DispatchQueue.main.async {
            // Process Forward Geocoding Response
            self?.processResponse(withPlacemarks: placemarks, error: error)
        }
    }
}

If the user's input is empty, we clear the table view. If the user enters a valid location, we invoke geocodeAddressString(_:completionHandler:) on the CLGeocoder instance. Core Location returns an array of CLPlacemark instances if the geocoding request is successful. The CLPlacemark class is defined in the Core Location framework and is used to store the metadata for a geographic location.

The response of the geocoding request is handled in the processResponse(withPlacemarks:error:) method. We create an array of Location objects from the array of CLPlacemark instances and update the table view. That's it.

AddLocationViewController.swift

private func processResponse(withPlacemarks placemarks: [CLPlacemark]?, error: Error?) {
    if let error = error {
        print("Unable to Forward Geocode Address (\(error))")

    } else if let matches = placemarks {
        // Update Locations
        locations = matches.compactMap({ (match) -> Location? in
            guard let name = match.name else { return nil }
            guard let location = match.location else { return nil }
            return Location(name: name, latitude: location.coordinate.latitude, longitude: location.coordinate.longitude)
        })

        // Update Table View
        tableView.reloadData()
    }
}

What's Next?

We covered the most important details of the AddLocationViewController class. How can we lift the view controller from some of its responsibilities? In the next episode, I show you what I have in mind.