Mastering MVVM With Swift

Taking MVVM to the Next Level

Resources

You should now have a good understanding of what MVVM is and how it can be used to cure some of the problems MVC suffers from. But we can do better. Up until now, data in the application has flown in one direction. The view controller asks the view model for data and populates the view it manages. This is fine and many projects can greatly benefit from this implementation of the Model-View-ViewModel pattern.

Data Flows in One Direction

But it's 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 the Model-View-ViewModel pattern 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 for the user 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 controller 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 controller.

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

The user enters the name of the city she would like to add and Cloudy uses the Core Location framework to forward geocode the name of that city. Under the hood, Cloudy asks the Core Location framework for the coordinates of the city the user has entered.

When the user enters a city in the search bar and taps the Search button, the view controller uses a CLGeocder instance to forward geocode the name of the city. Forward geocoding is an asynchronous operation. The view controller updates the 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 dramatically using the Model-View-ViewModel pattern. That's what you're here for.

How can we improve what we currently have? We'll create a view model 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's 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're only going to focus on the AddLocationViewController class.

The class defines outlets for a table view and a search bar. It also maintains a list of Location instances. These are the results the Core Location framework hands us. 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're not familiar with this class, it's 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: -

    var delegate: AddLocationViewControllerDelegate?

}

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

AddLocationViewController.swift

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

The add location view controller is 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 = LocationsViewLocationViewModel(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 hands us 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 instances 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.flatMap({ (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()
    }
}

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

Resources
Next Episode "What Are the Options"

Stop Writing Swift That Sucks

Download the Swift Patterns I Swear By