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.

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.