8 Rinse and Repeat

Creating the Week View View Model

To adopt the Model-View-ViewModel pattern in the WeekViewController class, we first need to create a new view model. Create a new file in the View Models group and name the file WeekViewViewModel.swift.

Creating the Week View View Model
Creating the Week View View Model

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

WeekViewViewModel.swift

1 import UIKit
2 
3 struct WeekViewViewModel {
4 
5 }

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 instances.

WeekViewViewModel.swift

1 import UIKit
2 
3 struct WeekViewViewModel {
4 
5     // MARK: - Properties
6 
7     let weatherData: [WeatherDayData]
8 
9 }

The WeekViewViewModel struct will look a bit different from the DayViewViewModel struct because it manages an array of model objects. Once we’re done refactoring the WeekViewController class, it will no longer have a reference to the weather data. This means that the week view controller won’t know how many sections and rows the 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 WeekViewViewModel struct, numberOfSections. The numberOfSections computed property returns the number of sections the week view controller should display in the table view it manages. The view controller currently display only one section, which means we return 1.

WeekViewViewModel.swift

1 var numberOfSections: Int {
2     return 1
3 }

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

WeekViewViewModel.swift

1 var numberOfDays: Int {
2     return weatherData.count
3 }

Up until now, we used computed properties to provide the view controller with the information it needs. It’s time for a few methods. These methods will provide the view controller with weather data for a particular day. This means that the view controller asks the view model for the weather data for a specific index. The index corresponds with a row in the table view.

Let’s start with the day label of the WeatherDayTableViewCell. The view controller will ask its view model for a string that represents the day of the weather data. This means we need to implement a method in the WeekViewViewModel struct that accepts an index of type Int and returns a value of type String.

WeekViewViewModel.swift

1 func day(for index: Int) -> String {
2 
3 }

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

WeekViewViewModel.swift

 1 func day(for index: Int) -> String {
 2     // Fetch Weather Data for Day
 3     let weatherDayData = weatherData[index]
 4 
 5     // Initialize Date Formatter
 6     let dateFormatter = DateFormatter()
 7 
 8     // Configure Date Formatter
 9     dateFormatter.dateFormat = "EEEE"
10 
11     return dateFormatter.string(from: weatherDayData.time)
12 }

As I mentioned earlier in this book, I prefer to make the DateFormatter instance a property of the view model. Let’s take care of that now. This is what the WeekViewViewModel struct looks like so far.

WeekViewViewModel.swift

 1 import UIKit
 2 
 3 struct WeekViewViewModel {
 4 
 5     // MARK: - Properties
 6 
 7     let weatherData: [WeatherDayData]
 8 
 9     // MARK: -
10 
11     private let dayFormatter = DateFormatter()
12 
13     // MARK: -
14 
15     var numberOfSections: Int {
16         return 1
17     }
18 
19     // MARK: - Methods
20 
21     func day(for index: Int) -> String {
22         // Fetch Weather Data for Day
23         let weatherDayData = weatherData[index]
24 
25         // Configure Date Formatter
26         dayFormatter.dateFormat = "EEEE"
27 
28         return dayFormatter.string(from: weatherDayData.time)
29     }
30 
31 }

We can use the same approach to populate the date label of the WeatherDayTableViewCell. Only the value of the date formatter’s dateFormat property is different. You could argue that we could get away with only one DateFormatter property. That’s a personal choice. It won’t dramatically impact performance since we only have a handful of table view cells to populate.

WeekViewViewModel.swift

 1 import UIKit
 2 
 3 struct WeekViewViewModel {
 4 
 5     // MARK: - Properties
 6 
 7     let weatherData: [WeatherDayData]
 8 
 9     // MARK: -
10 
11     private let dayFormatter = DateFormatter()
12     private let dateFormatter = DateFormatter()
13 
14     ...
15 
16     func date(for index: Int) -> String {
17         // Fetch Weather Data for Day
18         let weatherDayData = weatherData[index]
19 
20         // Configure Date Formatter
21         dateFormatter.dateFormat = "MMMM d"
22 
23         return dateFormatter.string(from: weatherDayData.time)
24     }
25 
26 }

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

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 as a result.

WeekViewViewModel.swift

1 func temperature(for index: Int) -> String {
2     // Fetch Weather Data
3     let weatherDayData = weatherData[index]
4 
5     let min = format(temperature: weatherDayData.temperatureMin)
6     let max = format(temperature: weatherDayData.temperatureMax)
7 
8     return "\(min) - \(max)"
9 }

The format(temperature:) helper method isn’t complicated. It only prevents us from repeating ourselves.

WeekViewViewModel.swift

 1 // MARK: - Helper Methods
 2 
 3 private func format(temperature: Double) -> String {
 4     switch UserDefaults.temperatureNotation() {
 5     case .fahrenheit:
 6         return String(format: "%.0f 째F", temperature)
 7     case .celsius:
 8         return String(format: "%.0f 째C", temperature.toCelcius())
 9     }
10 }

Populating the wind speed label and the icon image view is very similar to what we covered so far. Take a look at the implementations below.

WeekViewViewModel.swift

 1 func windSpeed(for index: Int) -> String {
 2     // Fetch Weather Data
 3     let weatherDayData = weatherData[index]
 4     let windSpeed = weatherDayData.windSpeed
 5 
 6     switch UserDefaults.unitsNotation() {
 7     case .imperial:
 8         return String(format: "%.f MPH", windSpeed)
 9     case .metric:
10         return String(format: "%.f KPH", windSpeed.toKPH())
11     }
12 }

WeekViewViewModel.swift

1 func image(for index: Int) -> UIImage? {
2     // Fetch Weather Data
3     let weatherDayData = weatherData[index]
4 
5     return UIImage.imageForIcon(withName: weatherDayData.icon)
6 }

Creating the View Model in the Root View Controller

With the WeekViewViewModel struct ready to use, we shift focus to the RootViewController class. First, however, we replace the week property in the WeekViewController class with a property named viewModel of type WeekViewViewModel?.

WeekViewController.swift

1 var viewModel: WeekViewViewModel? {
2     didSet {
3         updateView()
4     }
5 }

We can now update the RootViewController class. The root view controller no longer passes the array of WeatherDayData instances to the week view controller. Instead, the root view controller instantiates an instance of the WeekViewViewModel using the array of WeatherDayData instances 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, we create an instance of the WeekViewViewModel struct and assign it to the viewModel property of the week view controller.

RootViewController.swift

 1 private func fetchWeatherData() {
 2     ...
 3 
 4     dataManager.weatherDataForLocation(latitude: latitude, longitude\
 5 : longitude) { (response, error) in
 6         if let error = error {
 7             print(error)
 8         } else if let response = response {
 9             // Configure Day View Controller
10             self.dayViewController.viewModel = DayViewViewModel(weat\
11 herData: response)
12 
13             // Configure Week View Controller
14             self.weekViewController.viewModel = WeekViewViewModel(we\
15 atherData:  response.dailyData)
16         }
17     }
18 }

Updating the Table View

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

WeekViewController.swift

1 private func updateWeatherDataContainer() {
2     weatherDataContainer.isHidden = false
3 
4     tableView.reloadData()
5 }

We also update the updateView() method to reflect these changes. We also replace any references to the week property with references to the viewModel property.

WeekViewController.swift

 1 private func updateView() {
 2     activityIndicatorView.stopAnimating()
 3     tableView.refreshControl?.endRefreshing()
 4 
 5     if let _ = viewModel {
 6         updateWeatherDataContainer()
 7 
 8     } else {
 9      ...
10     }
11 }

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

WeekViewController.swift

1 func numberOfSections(in tableView: UITableView) -> Int {
2     guard let viewModel = viewModel else { return 0 }
3     return viewModel.numberOfSections
4 }

WeekViewController.swift

1 func tableView(_ tableView: UITableView, numberOfRowsInSection secti\
2 on: Int) -> Int {
3     guard let viewModel = viewModel else { return 0 }
4     return viewModel.numberOfDays
5 }

Thanks to the WeekViewViewModel struct, we can greatly simplify the implementation of tableView(_:cellForRowAt:).

WeekViewController.swift

 1 func tableView(_ tableView: UITableView, cellForRowAt indexPath: Ind\
 2 exPath) -> UITableViewCell {
 3     guard let cell = tableView.dequeueReusableCell(withIdentifier: W\
 4 eatherDayTableViewCell.reuseIdentifier, for: indexPath) as? WeatherD\
 5 ayTableViewCell else { fatalError("Unexpected Table View Cell") }
 6 
 7     if let viewModel = viewModel {
 8         // Configure Cell
 9         cell.dayLabel.text = viewModel.day(for: indexPath.row)
10         cell.dateLabel.text = viewModel.date(for: indexPath.row)
11         cell.iconImageView.image = viewModel.image(for: indexPath.ro\
12 w)
13         cell.windSpeedLabel.text = viewModel.windSpeed(for: indexPat\
14 h.row)
15         cell.temperatureLabel.text = viewModel.temperature(for: inde\
16 xPath.row)
17     }
18 
19     return cell
20 }

Last but not least, we can get rid of the DateFormatter properties of the WeekViewController. They’re no longer needed and that’s a very welcome change.

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