In the remainder of this series, we refactor an application that is built with the Model-View-Controller pattern and make it adopt the Model-View-ViewModel pattern instead. This will answer two important questions:

  • What are the shortcomings of the MVC pattern?
  • How can the MVVM pattern help resolve these shortcomings?

Cloudy is the application we refactor. It is a lightweight weather application that shows the user the weather for their current location or a saved location. It shows the current weather conditions and a forecast for the next few days. The weather data is retrieved from a weather API.

Meet Cloudy

The user can add locations and switch between locations by bringing up the locations view.

Managing Locations

Adding Locations

Cloudy has a settings view that enables the user to customize the presentation of the weather data. The user can customize the time, wind speed, and temperature notation in the settings view.

Managing the Application's Settings

Application Architecture

In this episode, I walk you through the source code of the project. You can follow along by opening the starter project of this episode.

Storyboard

The main storyboard is the best place to start. You can see that we have a container view controller with two child view controllers. The top child view controller shows the current weather conditions, the bottom child view controller displays the forecast for the next few days in a table view.

Cloudy's Main Storyboard

If the user taps the location button in the top child view controller (top left), the locations view is shown. The user can select a saved location or add a new location in the add location view.

Cloudy's Main Storyboard

If the user taps the settings button in the top child view controller (top right), the settings view is shown. This is another table view listing the options we discussed earlier.

Cloudy's Main Storyboard

View Controllers

If we open the View Controllers group in the Project Navigator, we can see the view controller classes that correspond with what I just showed you in the storyboard.

Cloudy's View Controllers

The RootViewController class is the container view controller. The DayViewController class is the top cild view controller and the WeekViewController class is the bottom child view controller. The WeatherViewController class is the superclass of the DayViewController and the WeekViewController classes.

Root View Controller

The root view controller is responsible for several tasks:

  • it fetches the weather data
  • it fetches the current location of the user's device
  • it sends the weather data to its child view controllers

The root view controller delegates the fetching of the weather data to the DataManager class. This class sends the request to the weather API and converts the JSON response to model objects.

In the completion handler of the weatherDataForLocation(latitude:longitude:completion:) method of the RootViewController class, the weather data is passed to the day and week view controllers.

RootViewController.swift

// Fetch Weather Data for Location
dataManager.weatherDataForLocation(latitude: latitude, longitude: longitude) { [weak self] (result) in
    switch result {
    case .success(let weatherData):
        // Configure Day View Controller
        self?.dayViewController.now = weatherData

        // Configure Week View Controller
        self?.weekViewController.week = weatherData.dailyData
    case .failure:
        // Notify User
        self?.presentAlert(of: .noWeatherDataAvailable)

        // Update Child View Controllers
        self?.dayViewController.now = nil
        self?.weekViewController.week = nil
    }
}

Model Objects

The model objects we will be working with are Location, WeatherData and WeatherDayData. You can find them in the Models group.

Model Objects

The Location structure makes working with locations a bit easier. There is no magic involved. The WeatherData and WeatherDayData structures contain the weather data that is fetched from the weather API. Notice that a WeatherData object contains an array of WeatherDayData objects.

WeatherData.swift

import Foundation

struct WeatherData: Decodable {

    ...

    // MARK: - Properties

    let time: Date

    // MARK: -

    let latitude: Double
    let longitude: Double
    let windSpeed: Double
    let temperature: Double

    // MARK: -

    let icon: String
    let summary: String

    // MARK: -

    let dailyData: [WeatherDayData]

    ...

}

The current weather conditions are stored in the WeatherData object and the forecast for the next few days is stored in an array of WeatherDayData objects.

The root view controller passes the week view controller the array of WeatherDayData objects, which it displays in a table view.

WeekViewController.swift

// Configure Week View Controller
self?.weekViewController.week = weatherData.dailyData

The day view controller receives the WeatherData object from the root view controller.

DayViewController.swift

// Configure Day View Controller
self?.dayViewController.now = weatherData

Day View Controller

The now property of the DayViewController class stores the WeatherData object. Every time this property is set, the user interface is updated with new weather data by invoking updateView().

DayViewController.swift

var now: WeatherData? {
    didSet {
        updateView()
    }
}

In updateView(), we hide the activity indicator view and update the weather data container view. The weather data container view is nothing more than a view that contains the views that present the weather data to the user.

DayViewController.swift

private func updateView() {
    activityIndicatorView.stopAnimating()

    if let now = now {
        updateWeatherDataContainerView(with: now)

    } else {
        messageLabel.isHidden = false
        messageLabel.text = "Cloudy was unable to fetch weather data."
    }
}

The implementation of updateWeatherDataContainerView(with:) is a classic example of the Model-View-Controller pattern. The model object is dissected and the raw values are transformed, formatted, and presented to the user.

DayViewController.swift

private func updateWeatherDataContainerView(with weatherData: WeatherData) {
    weatherDataContainerView.isHidden = false

    var windSpeed = weatherData.windSpeed
    var temperature = weatherData.temperature

    let dateFormatter = DateFormatter()
    dateFormatter.dateFormat = "EEE, MMMM d"
    dateLabel.text = dateFormatter.string(from: weatherData.time)

    let timeFormatter = DateFormatter()

    if UserDefaults.timeNotation == .twelveHour {
        timeFormatter.dateFormat = "hh:mm a"
    } else {
        timeFormatter.dateFormat = "HH:mm"
    }

    timeLabel.text = timeFormatter.string(from: weatherData.time)

    descriptionLabel.text = weatherData.summary

    if UserDefaults.temperatureNotation != .fahrenheit {
        temperature = temperature.toCelcius
        temperatureLabel.text = String(format: "%.1f °C", temperature)
    } else {
        temperatureLabel.text = String(format: "%.1f °F", temperature)
    }

    if UserDefaults.unitsNotation != .imperial {
        windSpeed = windSpeed.toKPH
        windSpeedLabel.text = String(format: "%.f KPH", windSpeed)
    } else {
        windSpeedLabel.text = String(format: "%.f MPH", windSpeed)
    }

    iconImageView.image = imageForIcon(withName: weatherData.icon)
}

Week View Controller

The week view controller looks similar in several ways. The week property stores the weather data and every time the property is set, the view controller's view is updated with the new weather data by invoking updateView().

WeekViewController.swift

var week: [WeatherDayData]? {
    didSet {
        updateView()
    }
}

In updateView(), we stop the activity indicator view, stop refreshing the refresh control, and invoke updateWeatherDataContainerView(with:) if there is weather data available.

WeekViewController.swift

private func updateView() {
    activityIndicatorView.stopAnimating()
    tableView.refreshControl?.endRefreshing()

    if let week = week {
        updateWeatherDataContainerView(with: week)

    } else {
        messageLabel.isHidden = false
        messageLabel.text = "Cloudy was unable to fetch weather data."
    }
}

In updateWeatherDataContainerView(with:), we show the weather data container, which contains the table view, and reload the table view.

WeekViewController.swift

private func updateWeatherDataContainerView(with weatherData: [WeatherDayData]) {
    // Show Weather Data Container View
    weatherDataContainerView.isHidden = false

    // Update Table View
    tableView.reloadData()
}

The most interesting aspect of the week view controller is the configuration of table view cells in tableView(_:cellForRowAt:). In this method, we dequeue a table view cell, fetch the weather data for the day that corresponds with the index path, and configure the table view cell.

WeekViewController.swift

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

    if let week = week {
        // Fetch Weather Data
        let weatherData = week[indexPath.row]

        var windSpeed = weatherData.windSpeed
        var temperatureMin = weatherData.temperatureMin
        var temperatureMax = weatherData.temperatureMax

        if UserDefaults.temperatureNotation != .fahrenheit {
            temperatureMin = temperatureMin.toCelcius
            temperatureMax = temperatureMax.toCelcius
        }

        // Configure Cell
        cell.dayLabel.text = dayFormatter.string(from: weatherData.time)
        cell.dateLabel.text = dateFormatter.string(from: weatherData.time)

        let min = String(format: "%.0f°", temperatureMin)
        let max = String(format: "%.0f°", temperatureMax)

        cell.temperatureLabel.text = "\(min) - \(max)"

        if UserDefaults.unitsNotation != .imperial {
            windSpeed = windSpeed.toKPH
            cell.windSpeedLabel.text = String(format: "%.f KPH", windSpeed)
        } else {
            cell.windSpeedLabel.text = String(format: "%.f MPH", windSpeed)
        }

        cell.iconImageView.image = imageForIcon(withName: weatherData.icon)
    }

    return cell
}

As in the day view controller, we take the raw values of the model objects and format them before displaying the weather data to the user. Notice that we use several if statements to make sure the weather data is formatted according to the user's preferences.

Locations View Controller

The locations view controller manages a list of locations and it displays the coordinates of the current location of the device. If the user selects a location from the list, Cloudy asks the weather API for that location's weather data and displays it in the weather view controllers.

The user can add a new location by tapping the plus button in the top left. This summons the add location view controller. The user is asked to enter the name of a city. Under the hood, the add location view controller uses the Core Location framework to perform a forward geocoding request. Cloudy is only interested in the coordinates of any matches the Core Location framework returns.

Settings View Controller

Despite the simplicity of the settings view, the SettingsViewController class is almost 200 lines long. Later in this series, we leverage the Model-View-ViewModel pattern to make its implementation shorter and more transparent.

The SettingsViewController class has a delegate, which it notifies whenever a setting changed.

SettingsViewController.swift

protocol SettingsViewControllerDelegate {
    func controllerDidChangeTimeNotation(controller: SettingsViewController)
    func controllerDidChangeUnitsNotation(controller: SettingsViewController)
    func controllerDidChangeTemperatureNotation(controller: SettingsViewController)
}

The root view controller is the delegate of the settings view controller and it tells its child view controllers to reload their user interface whenever a setting changed.

RootViewController.swift

extension RootViewController: SettingsViewControllerDelegate {

    func controllerDidChangeTimeNotation(controller: SettingsViewController) {
        dayViewController.reloadData()
        weekViewController.reloadData()
    }

    func controllerDidChangeUnitsNotation(controller: SettingsViewController) {
        dayViewController.reloadData()
        weekViewController.reloadData()
    }

    func controllerDidChangeTemperatureNotation(controller: SettingsViewController) {
        dayViewController.reloadData()
        weekViewController.reloadData()
    }

}

Time to Write Some Code

That is all you need to know about Cloudy for now. In the next episode, we focus on several aspects in more detail and discuss which bits we plan to refactor with the help of the Model-View-ViewModel pattern.