In this episode, we populate the week view. We build the user interface and implement the WeekViewModel struct and the WeekViewController class. Let's start with the user interface.

Building the User Interface

Open WeekViewController.swift and declare an outlet for a table view. Remember that the week view controller shows the user a weather forecast of the coming days. Each table view cell of the table view displays the weather forecast for a specific day.

import UIKit

final class WeekViewController: UIViewController {

    // MARK: - Properties

    var viewModel: WeekViewModel? {
        didSet {
            guard let viewModel = viewModel else {
                return
            }

            // Setup View Model
            setupViewModel(with: viewModel)
        }
    }

    // MARK: -

    @IBOutlet var tableView: UITableView!

    ...

}

We configure the table view in a didSet property observer. The table view should be hidden as long as there's no data to display. The view controller acts as the data source of the table view. We set the separator inset to zero to make sure it spans the width of the table view.

The height of the rows of the table view is driven by Auto Layout. For that to work, we assign a sensible value to the estimatedRowHeight property and we set the rowHeight property to UITableViewAutomaticDimension. Auto Layout takes care of the rest.

Because the table view displays a handful of table view cells, there's no need to display a vertical scroll indicator. A scroll indicator only makes sense if the table view spans dozens or hundreds of rows.

@IBOutlet var tableView: UITableView! {
    didSet {
        tableView.isHidden = true
        tableView.dataSource = self
        tableView.separatorInset = .zero
        tableView.estimatedRowHeight = 44.0
        tableView.rowHeight = UITableViewAutomaticDimension
        tableView.showsVerticalScrollIndicator = false
    }
}

We also define an outlet for an activity indicator view. We show the activity indicator view as long as there's no data to display to the user.

import UIKit

final class WeekViewController: UIViewController {

    // MARK: - Properties

    var viewModel: WeekViewModel? {
        didSet {
            guard let viewModel = viewModel else {
                return
            }

            // Setup View Model
            setupViewModel(with: viewModel)
        }
    }

    // MARK: -

    @IBOutlet var tableView: UITableView! {
        didSet {
            tableView.isHidden = true
            tableView.dataSource = self
            tableView.separatorInset = .zero
            tableView.estimatedRowHeight = 44.0
            tableView.rowHeight = UITableViewAutomaticDimension
            tableView.showsVerticalScrollIndicator = false
        }
    }

    // MARK: -

    @IBOutlet var activityIndicatorView: UIActivityIndicatorView!

    ...

}

We configure the activity indicator view in a didSet property observer. It should start animating the moment the application is launched and it should hide itself when we instruct it to stop animating.

@IBOutlet var activityIndicatorView: UIActivityIndicatorView! {
    didSet {
        activityIndicatorView.startAnimating()
        activityIndicatorView.hidesWhenStopped = true
    }
}

To keep the compiler happy, we need to conform the WeekViewController class to the UITableViewDataSource protocol. Before we take care of that task, though, we create a UITableViewCell subclass for displaying the weather conditions for a specific day.

Create a new group, Table View Cells, in the Week View Controller group. Create a UITableViewCell subclass in the Table View Cells group and name it WeekDayTableViewCell. We implement the WeekDayTableViewCell class later in this episode. For now, we define a static computed property, reuseIdentifier, of type String, that returns the reuse identifier of the table view cell.

import UIKit

class WeekDayTableViewCell: UITableViewCell {

    // MARK: - Static Properties

    static var reuseIdentifier: String {
        return String(describing: self)
    }

    // MARK: - Initialization

    override func awakeFromNib() {
        super.awakeFromNib()
    }

}

In the awakeFromNib() method, we set the table view cell's selectionStyle property to none. The user won't be able to select the table view cells of the table view.

import UIKit

class WeekDayTableViewCell: UITableViewCell {

    // MARK: - Static Properties

    static var reuseIdentifier: String {
        return String(describing: self)
    }

    // MARK: - Initialization

    override func awakeFromNib() {
        super.awakeFromNib()

        // Configure Cell
        selectionStyle = .none
    }

}

Revisit WeekViewController.swift and create an extension for the WeekViewController class. We use the extension to conform the WeekViewController class to the UITableViewDataSource protocol.

extension WeekViewController: UITableViewDataSource {

}

We implement two methods of the UITableViewDataSource protocol, tableView(_:numberOfRowsInSection:) and tableView(_:cellForRowAt:). We return 0 from tableView(_:numberOfRowsInSection:) for the time being.

extension WeekViewController: UITableViewDataSource {

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

}

In tableView(_:cellForRowAt:), we ask the table view for a table view cell with the reuse identifier of the WeekDayTableViewCell class. We configure the table view cell later in this episode. I always use a guard statement if I need to dequeue a UITableViewCell subclass. If the cast to the subclass is unsuccessful, I throw a fatal error because that should never happen. This is a convenient and elegant pattern to avoid optionals in tableView(_:cellForRowAt:).

extension WeekViewController: UITableViewDataSource {

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

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

        return cell
    }

}

Open Main.storyboard and locate the week view controller. Add an activity indicator view to the view controller's view and center it in its superview. Connect the activity indicator view to the outlet we defined a few minutes ago.

Adding an Activity Indicator View

We also need to add a table view to the week view controller. Pin it to the edges of the view controller's view and don't forget to connect the table view to the tableView outlet of the WeekViewController class.

Adding a Table View

We set the number of prototype cells to 1. Select the prototype cell, open the Identity Inspector on the right, and set Class to WeekDayTableViewCell.

Configuring a Prototype Cell

Open the Attributes Inspector and set the reuse identifier of the prototype cell to WeekDayTableViewCell.

Configuring a Prototype Cell

Before we run the application, we need to update the setupViewModel(with:) method. Because this helper method is invoked when the week view controller has data to display, we hide the activity indicator view and show the table view. We also invoke reloadData() on the table view to update what it's displaying.

private func setupViewModel(with viewModel: WeekViewModel) {
    // Hide Activity Indicator View
    activityIndicatorView.stopAnimating()

    // Update Table View
    tableView.reloadData()
    tableView.isHidden = false
}

Last but not least, set the background color of the view controller's view to white in setupView().

private func setupView() {
    // Configure View
    view.backgroundColor = .white
}

Run the application to see the result. You should see an activity indicator view when the application launches. The moment the application has fetched weather data from the Dark Sky API, the activity indicator view disappears and an empty table view is shown to the user. It's time to populate the table view with weather data.

Populating the Week View Controller

Populating the Table View

Even though the view controller acts as the data source of the table view, remember that the view model manages the model. This isn't a problem, though. The view model of the week view controller should expose a simple interface that provides the view controller with the information it needs to populate its table view. Let's start with the number of rows in each section.

Open WeekViewModel.swift. We won't be returning the number of rows in each section. We keep it simple and return the number of days for which the week view model has weather data. The numberOfDays computed property is of type Int and it returns the number of items in the weatherData array.

import Foundation

struct WeekViewModel {

    // MARK: - Properties

    let weatherData: [ForecastWeatherConditions]

    // MARK: -

    var numberOfDays: Int {
        return weatherData.count
    }

}

With the numberOfDays computed property in place, we can update the implementation of the UITableViewDataSource protocol in WeekViewController.swift. If the viewModel property doesn't have a value, we return 0.

extension WeekViewController: UITableViewDataSource {

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

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

        return cell
    }

}

Let's switch gears and focus on the user interface of the WeekDayTableViewCell class. Open WeekDayTableViewCell.swift. A WeekDayTableViewCell instance should display the day of the week, the date, the minimum and maximum temperature, the wind speed, and an icon that visualizes the weather conditions for that day.

We start by creating an outlet for each user interface element. We use didSet property observers to configure the labels and the image view. The styles we defined earlier in this series help us to keep the user interface consistent and they also avoid the need for object literals.

import UIKit

class WeekDayTableViewCell: UITableViewCell {

    // MARK: - Static Properties

    static var reuseIdentifier: String {
        return String(describing: self)
    }

    // MARK: - Properties

    @IBOutlet var dayLabel: UILabel! {
        didSet {
            dayLabel.textColor = UIColor.Rainstorm.baseTextColor
            dayLabel.font = UIFont.Rainstorm.heavyLarge
        }
    }

    @IBOutlet var dateLabel: UILabel! {
        didSet {
            dateLabel.textColor = .black
            dateLabel.font = UIFont.Rainstorm.lightRegular
        }
    }

    @IBOutlet var windSpeedLabel: UILabel! {
        didSet {
            windSpeedLabel.textColor = .black
            windSpeedLabel.font = UIFont.Rainstorm.lightSmall
        }
    }

    @IBOutlet var temperatureLabel: UILabel! {
        didSet {
            temperatureLabel.textColor = .black
            temperatureLabel.font = UIFont.Rainstorm.lightSmall
        }
    }

    @IBOutlet var iconImageView: UIImageView! {
        didSet {
            iconImageView.contentMode = .scaleAspectFit
            iconImageView.tintColor = UIColor.Rainstorm.baseTintColor
        }
    }

    // MARK: - Initialization

    override func awakeFromNib() {
        super.awakeFromNib()

        // Configure Cell
        selectionStyle = .none
    }

}

Open Main.storyboard and add four labels and an image view to the content view of the prototype cell. We add constraints to each of the user interface elements to define their size and position.

Select the prototype cell in the Document Outline on the left and connect each user interface element to its corresponding outlet in the Connections Inspector on the right.

Building the User Interface

To make it straightforward to populate a WeekDayTableViewCell instance, we create a view model for the WeekDayTableViewCell class. Add a Swift file to the View Models group and name it WeekDayViewModel.swift. Replace the import statement for Foundation with an import statement for UIKit and define a struct with name WeekDayViewModel.

import UIKit

struct WeekDayViewModel {

}

The WeekDayViewModel struct manages an object that conforms to the ForecastWeatherConditions protocol. Define a property, weatherData, of type ForecastWeatherConditions.

import UIKit

struct WeekDayViewModel {

    // MARK: - Properties

    let weatherData: ForecastWeatherConditions

}

The implementation looks similar to that of DayViewModel. We define a private, constant property, dateFormatter, of type DateFormatter.

import UIKit

struct WeekDayViewModel {

    // MARK: - Properties

    let weatherData: ForecastWeatherConditions

    // MARK: -

    private let dateFormatter = DateFormatter()

}

The interface of the WeekDayTableViewCell class shows us what the interface of the WeekDayViewModel struct should look like. We start by implementing a computed property, day, of type String to populate the day label. We set the dateFormat property of the date formatter and ask it to convert the time property of the weather data to a string.

var day: String {
    // Configure Date Formatter
    dateFormatter.dateFormat = "EEEE"

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

Populating the date label is almost identical. The only difference is the date format of the date formatter.

var date: String {
    // Configure Date Formatter
    dateFormatter.dateFormat = "MMMM d"

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

To populate the temperature label, we ask the weather data for the minimum and the maximum temperature. We format the values and combine the strings using string interpolation.

var temperature: String {
    let min = String(format: "%.1f °F", weatherData.temperatureMin)
    let max = String(format: "%.1f °F", weatherData.temperatureMax)

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

The implementations of windSpeed and image are identical to those of the DayViewModel struct.

var windSpeed: String {
    return String(format: "%.f MPH", weatherData.windSpeed)
}

var image: UIImage? {
    return UIImage.imageForIcon(with: weatherData.icon)
}

We can now use the WeekDayViewModel struct to populate a WeekDayTableViewCell instance. Open WeekDayTableViewCell.swift and define an internal method, configure(with:). It accepts an instance of the WeekDayViewModel struct as its only argument. In the body of the configure(with:) method, we use the view model to populate the table view cell.

// MARK: - Public API

func configure(with viewModel: WeekDayViewModel) {
    dayLabel.text = viewModel.day
    dateLabel.text = viewModel.date
    iconImageView.image = viewModel.image
    windSpeedLabel.text = viewModel.windSpeed
    temperatureLabel.text = viewModel.temperature
}

The WeekViewModel struct manages the array of ForecastWeatherConditions objects and it makes sense to put the WeekViewModel struct in charge of creating a WeekDayViewModel instance for each table view cell. Open WeekViewModel.swift and define a method, viewModel(for:), that accepts one argument of type Int. The method returns a WeekDayViewModel instance. The implementation is short and straightforward.

func viewModel(for index: Int) -> WeekDayViewModel {
    return WeekDayViewModel(weatherData: weatherData[index])
}

To implement the last piece of the puzzle, we need to revisit WeekViewController.swift. We need to update tableView(_:cellForRowAt:). We unwrap the value of the viewModel property using a guard statement. Because the viewModel property should never be equal to nil in tableView(_:cellForRowAt:), we throw a fatal error if it doesn't have a value.

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

    guard let viewModel = viewModel else {
        fatalError("No View Model Present")
    }

    return cell
}

We ask the view model for a view model for the table view cell and use it to configure the table view cell.

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

    guard let viewModel = viewModel else {
        fatalError("No View Model Present")
    }

    // Configure Cell
    cell.configure(with: viewModel.viewModel(for: indexPath.row))

    return cell
}

Run the application to see the result. The table view of the week view controller should be populated with weather data.

Populating the Week View Controller

Protocol-Oriented Programming

We can make a small improvement to the implementation of the WeekDayTableViewCell class. There's no need for the WeekDayTableViewCell class to know about the WeekDayViewModel struct. It only needs an object that exposes an interface that allows it to populate its user interface. A protocol is a perfect fit in this scenario.

Create a new group, Protocols, in Week View Controller and add a Swift file. Name the file WeekDayRepresentable.swift. Add an import statement for UIKit and define a protocol with name WeekDayRepresentable.

import UIKit

protocol WeekDayRepresentable {

}

The interface of the WeekDayRepresentable protocol mimics that of the WeekDayViewModel struct.

import UIKit

protocol WeekDayRepresentable {

    var day: String { get }
    var date: String { get }
    var temperature: String { get }
    var windSpeed: String { get }
    var image: UIImage? { get }

}

To put the protocol to use, we need two more ingredients. First, we need to conform the WeekDayViewModel struct to the WeekDayRepresentable protocol. This is simple. We create an extension for the WeekDayViewModel struct in WeekDayViewModel.swift and use it to conform it to the WeekDayRepresentable protocol. We can leave the extension empty because WeekDayViewModel already conforms to the WeekDayRepresentable protocol.

import UIKit

struct WeekDayViewModel {

    ...

}

extension WeekDayViewModel: WeekDayRepresentable {

}

Second, we need to update the configure(with:) method of the WeekDayTableViewCell class. It no longer accepts an instance of the WeekDayViewModel struct. Instead, it accepts an object that conforms to the WeekDayRepresentable protocol.

func configure(with representable: WeekDayRepresentable) {
    dayLabel.text = representable.day
    dateLabel.text = representable.date
    iconImageView.image = representable.image
    windSpeedLabel.text = representable.windSpeed
    temperatureLabel.text = representable.temperature
}

That's it. The WeekDayTableViewCell class no longer knows about the WeekDayViewModel struct. The only requirement of the configure(with:) method is that the object passed to the method conforms to the WeekDayRepresentable protocol. Run the application to make sure we didn't break anything.

What's Next?

With the WeekViewModel and WeekDayViewModel structs implemented, it's time to write a few more unit tests. That's the focus of the next episode.