In this episode, we shift focus to the WeekViewController class. To adopt the Model-View-ViewModel pattern in the WeekViewController class, we start by creating a type for the view model of the week view controller. Create a new file in the View Models group and name the file WeekViewModel.swift.

Creating the Week View Model

Replace the import statement for Foundation with an import statement for UIKit and declare the WeekViewModel struct.

WeekViewModel.swift

import UIKit

struct WeekViewModel {

}

You already know what the next step is. We need to create a property for the weather data the view model manages. We name the property weatherData and the property is of type array of WeatherDayData objects.

WeekViewModel.swift

import UIKit

struct WeekViewModel {

    // MARK: - Properties

    let weatherData: [WeatherDayData]

}

The WeekViewModel struct will look a bit different from the DayViewModel struct because it manages an array of model objects. Like the DayViewController class, the WeekViewController class should not know about the weather data it displays in its view. This implies that it doesn't know how many sections and rows its table view should display. That information needs to come from the view controller's view model.

Let's start by adding a new computed property to the WeekViewModel struct, numberOfSections. The numberOfSections computed property returns the number of sections the week view controller should display in its table view. The week view controller currently displays one section.

WeekViewModel.swift

var numberOfSections: Int {
    1
}

To populate the table view, the week view controller also needs to know how many rows each section contains. To answer that question, we need another computed property, numberOfDays, which tells the week view controller the number of days the view model has weather data for. We could implement a more sophisticated method that accepts the index of the section in the table view, but I prefer to keep view models as simple and as dumb as possible. The week view controller and its view model should only be given information they absolutely need to carry out their tasks.

WeekViewModel.swift

var numberOfDays: Int {
    weatherData.count
}

Up until now, we used computed properties to provide the view controller with the information it needs. It is time for a few methods. These methods provide the week view controller with weather data for a particular day. The week view controller asks the view model for the weather data for a particular row in the table view.

Let's start with the day label of the WeatherDayTableViewCell class. The week view controller asks its view model for a string that represents the day of the weather data. We need to implement a method in the WeekViewModel struct that accepts an index of type Int and returns a value of type String.

WeekViewModel.swift

func day(for index: Int) -> String {

}

We first fetch the WeatherDayData object that corresponds with the index that is passed to the day(for:) method. We create a DateFormatter instance and format the value of the model's time property.

WeekViewModel.swift

func day(for index: Int) -> String {
    // Fetch Weather Data for Day
    let weatherDayData = weatherData[index]

    // Initialize Date Formatter
    let dateFormatter = DateFormatter()

    // Configure Date Formatter
    dateFormatter.dateFormat = "EEEE"

    return dateFormatter.string(from: weatherDayData.time)
}

As I mentioned earlier in this series, I prefer to make the DateFormatter instance a property of the view model. Let's take care of that now. We define a private, constant property with name dateFormatter. We create and assign a DateFormatter instance to the dateFormatter property.

WeekViewModel.swift

import UIKit

struct WeekViewModel {

    // MARK: - Properties

    let weatherData: [WeatherDayData]

    // MARK: -

    private let dateFormatter = DateFormatter()

    // MARK: - Public API

    var numberOfSections: Int {
        1
    }

    var numberOfDays: Int {
        weatherData.count
    }

    func day(for index: Int) -> String {
        // Fetch Weather Data for Day
        let weatherDayData = weatherData[index]

        // Configure Date Formatter
        dateFormatter.dateFormat = "EEEE"

        return dateFormatter.string(from: weatherDayData.time)
    }

}

We can use the same approach to populate the date label of the WeatherDayTableViewCell class. The only difference is the date format of the date formatter.

WeekViewModel.swift

import UIKit

struct WeekViewModel {

    // MARK: - Properties

    let weatherData: [WeatherDayData]

    // MARK: -

    private let dateFormatter = DateFormatter()

    // MARK: - Public API

    ...

    func date(for index: Int) -> String {
        // Fetch Weather Data for Day
        let weatherDayData = weatherData[index]

        // Configure Date Formatter
        dateFormatter.dateFormat = "MMMM d"

        return dateFormatter.string(from: weatherDayData.time)
    }

}

Setting the text property of the temperature label of the WeatherDayTableViewCell class is another fine example of the elegance and versatility of view models. Remember that the WeatherDayTableViewCell class displays the minimum and the maximum temperature for a particular day. The view model should provide the formatted string to the week view controller so that it can pass it to the WeatherDayTableViewCell instance.

In the temperature(for:) method, we fetch the weather data, format the minimum and maximum temperatures using a helper method, format(temperature:), and return the formatted string.

WeekViewModel.swift

func temperature(for index: Int) -> String {
    // Fetch Weather Data
    let weatherDayData = weatherData[index]

    let min = format(temperature: weatherDayData.temperatureMin)
    let max = format(temperature: weatherDayData.temperatureMax)

    return "\(min) - \(max)"
}

The format(temperature:) helper method isn't complicated. It makes sure we don't repeat ourselves.

WeekViewModel.swift

// MARK: - Helper Methods

private func format(temperature: Double) -> String {
    switch UserDefaults.temperatureNotation {
    case .fahrenheit:
        return String(format: "%.0f °F", temperature)
    case .celsius:
        return String(format: "%.0f °C", temperature.toCelcius)
    }
}

Populating the wind speed label and the icon image view is similar to what we covered so far.

WeekViewModel.swift

func windSpeed(for index: Int) -> String {
    // Fetch Weather Data
    let weatherDayData = weatherData[index]
    let windSpeed = weatherDayData.windSpeed

    switch UserDefaults.unitsNotation {
    case .imperial:
        return String(format: "%.f MPH", windSpeed)
    case .metric:
        return String(format: "%.f KPH", windSpeed.toKPH)
    }
}

WeekViewModel.swift

func image(for index: Int) -> UIImage? {
    // Fetch Weather Data
    let weatherDayData = weatherData[index]

    return UIImage.imageForIcon(with: weatherDayData.icon)
}

Creating the View Model in the Root View Controller

With the WeekViewModel struct ready to use, we shift focus to the RootViewController class. Before we do that, we remove the week property from the WeekViewController class and define a property, viewModel, of type WeekViewModel?. We define a didSet property observer for the viewModel property. Every time the viewModel property is set, updateView() is invoked.

WeekViewController.swift

var viewModel: WeekViewModel? {
    didSet {
        updateView()
    }
}

We can now update the RootViewController class. The root view controller no longer passes the array of WeatherDayData objects to the week view controller. Instead, the root view controller creates a WeekViewModel object using the array of WeatherDayData objects and sets the viewModel property of the week view controller.

Open RootViewController.swift and navigate to the fetchWeatherData() method. In the completion handler of the weatherDataForLocation(latitude:longitude:completion:) method, the root view controller creates a WeekViewModel object and assigns it to the viewModel property of the week view controller. If the data manager fails to fetch weather data, we set the viewModel property of the week view controller to nil.

RootViewController.swift

private func fetchWeatherData() {
    ...

    // 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.viewModel = DayViewModel(weatherData: weatherData)

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

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

Updating the Table View

Revisit WeekViewController.swift. With the WeekViewModel struct ready to use, it is time to refactor the WeekViewController class. This means we need to update the updateWeatherDataContainerView(with:) method. We start by renaming this method to updateWeatherDataContainerView(). There is no need to pass in the view model like we did in the DayViewController class.

WeekViewController.swift

private func updateWeatherDataContainerView() {
    // Show Weather Data Container View
    weatherDataContainerView.isHidden = false

    // Update Table View
    tableView.reloadData()
}

We also update the updateView() method to reflect these changes.

WeekViewController.swift

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

    if viewModel != nil {
        updateWeatherDataContainerView()

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

The implementation of the UITableViewDataSource protocol also needs some changes. As you can see, we use the methods we implemented earlier in the WeekViewModel struct.

WeekViewController.swift

func numberOfSections(in tableView: UITableView) -> Int {
    viewModel?.numberOfSections ?? 0
}

WeekViewController.swift

func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    viewModel?.numberOfDays ?? 0
}

Thanks to the WeekViewModel struct, we can drastically simplify the implementation of the tableView(_:cellForRowAt:) method of the UITableViewDataSource protocol.

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 viewModel = viewModel {
        // Configure Cell
        cell.dayLabel.text = viewModel.day(for: indexPath.row)
        cell.dateLabel.text = viewModel.date(for: indexPath.row)
        cell.iconImageView.image = viewModel.image(for: indexPath.row)
        cell.windSpeedLabel.text = viewModel.windSpeed(for: indexPath.row)
        cell.temperatureLabel.text = viewModel.temperature(for: indexPath.row)
    }

    return cell
}

Last but not least, we can get rid of the DateFormatter properties of the WeekViewController class. These are no longer needed and that is a very welcome change. A controller shouldn't need to worry about date formatting.

Build and Run

Run the application to make sure we didn't break anything. If something doesn't look quite right, then you probably made a mistake in the view model.

What's Next?

Even though we successfully implemented the Model-View-ViewModel pattern in the week view controller, later in this series, we use protocols to further simplify the implementation of the Model-View-ViewModel pattern in the week view controller.