3 Meet Cloudy

In the remainder of this book, we’re going to refactor an application that’s built with MVC and make it adopt MVVM instead. This will teach you two important lessons:

The application we’re going to refactor is Cloudy. Cloudy is a lightweight weather application that shows the user the weather of 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 the Dark Sky API, an easy-to-use weather service.

Meet Cloudy
Meet Cloudy

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

Managing Locations
Managing Locations
Adding Locations
Adding Locations

Cloudy has a settings view to change the time notation, the application’s units system, and the user can switch between degrees Fahrenheit and degrees Celcius.

Managing the Application's Settings
Managing the Application’s Settings

Application Architecture

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

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
Cloudy’s Main Storyboard

If the user taps the location button in the top child view controller (top left), the locations view controller is shown. The user can switch between locations and add new locations using the add location view controller.

Cloudy's Main Storyboard
Cloudy’s Main Storyboard

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

Cloudy's Main Storyboard
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
Cloudy’s View Controllers

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

Root View Controller

The root view controller is responsible for several tasks:

The root view controller delegates the fetching of the weather data to the DataManager class. This class sends the request to the Dark Sky API and converts the JSON response to model objects. I use a simple, lightweight JSON parser for this task. The implementation of the JSON parser and the DataManager class are unimportant for this discussion.

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

RootViewController.swift

 1 dataManager.weatherDataForLocation(latitude: latitude, longitude: lo\
 2 ngitude) { (response, error) in
 3     if let error = error {
 4         print(error)
 5     } else if let response = response {
 6         // Configure Day View Controller
 7         self.dayViewController.now = response
 8 
 9         // Configure Week View Controller
10         self.weekViewController.week = response.dailyData
11     }
12 }

Model Objects

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

Model Objects
Model Objects

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

WeatherData.swift

 1 import Foundation
 2 
 3 struct WeatherData {
 4 
 5     let time: Date
 6 
 7     let lat: Double
 8     let long: Double
 9     let windSpeed: Double
10     let temperature: Double
11 
12     let icon: String
13     let summary: String
14 
15     let dailyData: [WeatherDayData]
16 
17 }

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 only hands the week view controller the array of WeatherDayData objects, which it displays in a table view.

WeekViewController.swift

1 var week: [WeatherDayData]?

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

DayViewController.swift

1 var 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

1 var now: WeatherData? {
2     didSet {
3         updateView()
4     }
5 }

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

DayViewController.swift

 1 private func updateView() {
 2     activityIndicatorView.stopAnimating()
 3 
 4     if let now = now {
 5         updateWeatherDataContainer(withWeatherData: now)
 6 
 7     } else {
 8         messageLabel.isHidden = false
 9         messageLabel.text = "Cloudy was unable to fetch weather data\
10 ."
11 
12     }
13 }

The implementation of updateWeatherDataContainer(withWeatherData:) is a classic example of the Model-View-Controller pattern. The model object is torn apart and the raw values are transformed and formatted for display to the user.

DayViewController.swift

 1 private func updateWeatherDataContainer(withWeatherData weatherData:\
 2  WeatherData) {
 3     weatherDataContainer.isHidden = false
 4 
 5     var windSpeed = weatherData.windSpeed
 6     var temperature = weatherData.temperature
 7 
 8     let dateFormatter = DateFormatter()
 9     dateFormatter.dateFormat = "EEE, MMMM d"
10     dateLabel.text = dateFormatter.string(from: weatherData.time)
11 
12     let timeFormatter = DateFormatter()
13 
14     if UserDefaults.timeNotation() == .twelveHour {
15         timeFormatter.dateFormat = "hh:mm a"
16     } else {
17         timeFormatter.dateFormat = "HH:mm"
18     }
19 
20     timeLabel.text = timeFormatter.string(from: weatherData.time)
21 
22     descriptionLabel.text = weatherData.summary
23 
24     if UserDefaults.temperatureNotation() != .fahrenheit {
25         temperature = temperature.toCelcius()
26         temperatureLabel.text = String(format: "%.1f °C", temperatur\
27 e)
28     } else {
29         temperatureLabel.text = String(format: "%.1f °F", temperatur\
30 e)
31     }
32 
33     if UserDefaults.unitsNotation() != .imperial {
34         windSpeed = windSpeed.toKPH()
35         windSpeedLabel.text = String(format: "%.f KPH", windSpeed)
36     } else {
37         windSpeedLabel.text = String(format: "%.f MPH", windSpeed)
38     }
39 
40     iconImageView.image = imageForIcon(withName: weatherData.icon)
41 }

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

1 var week: [WeatherDayData]? {
2     didSet {
3         updateView()
4     }
5 }

In updateView(), we stop the activity indicator view, stop refreshing the refresh control, and invoke updateWeatherDataContainer(withWeatherData:) if there’s weather data we need to show the user.

WeekViewController.swift

 1 private func updateView() {
 2     activityIndicatorView.stopAnimating()
 3     tableView.refreshControl?.endRefreshing()
 4 
 5     if let week = week {
 6         updateWeatherDataContainer(withWeatherData: week)
 7 
 8     } else {
 9         messageLabel.isHidden = false
10         messageLabel.text = "Cloudy was unable to fetch weather data\
11 ."
12         
13     }
14 }

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

WeekViewController.swift

1 private func updateWeatherDataContainer(withWeatherData weatherData:\
2  [WeatherDayData]) {
3     weatherDataContainer.isHidden = false
4 
5     tableView.reloadData()
6 }

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 populate the table view cell.

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 week = week {
 8         // Fetch Weather Data
 9         let weatherData = week[indexPath.row]
10 
11         var windSpeed = weatherData.windSpeed
12         var temperatureMin = weatherData.temperatureMin
13         var temperatureMax = weatherData.temperatureMax
14 
15         if UserDefaults.temperatureNotation() != .fahrenheit {
16             temperatureMin = temperatureMin.toCelcius()
17             temperatureMax = temperatureMax.toCelcius()
18         }
19 
20         // Configure Cell
21         cell.dayLabel.text = dayFormatter.string(from: weatherData.t\
22 ime)
23         cell.dateLabel.text = dateFormatter.string(from: weatherData\
24 .time)
25 
26         let min = String(format: "%.0f°", temperatureMin)
27         let max = String(format: "%.0f°", temperatureMax)
28 
29         cell.temperatureLabel.text = "\(min) - \(max)"
30 
31         if UserDefaults.unitsNotation() != .imperial {
32             windSpeed = windSpeed.toKPH()
33             cell.windSpeedLabel.text = String(format: "%.f KPH", win\
34 dSpeed)
35         } else {
36             cell.windSpeedLabel.text = String(format: "%.f MPH", win\
37 dSpeed)
38         }
39 
40         cell.iconImageView.image = imageForIcon(withName: weatherDat\
41 a.icon)
42     }
43 
44     return cell
45 }

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 based on the user’s preferences in the settings view controller.

Locations View Controller

The locations view controller manages a list of locations and it displays the coordinates of the device’s current location. If the user selects a location from the list, Cloudy asks the Dark Sky 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 book, we attempt to use 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 changes.

SettingsViewController.swift

1 protocol SettingsViewControllerDelegate {
2     func controllerDidChangeTimeNotation(controller: SettingsViewCon\
3 troller)
4     func controllerDidChangeUnitsNotation(controller: SettingsViewCo\
5 ntroller)
6     func controllerDidChangeTemperatureNotation(controller: Settings\
7 ViewController)
8 }

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

RootViewController.swift

 1 extension RootViewController: SettingsViewControllerDelegate {
 2 
 3     func controllerDidChangeTimeNotation(controller: SettingsViewCon\
 4 troller) {
 5         dayViewController.reloadData()
 6         weekViewController.reloadData()
 7     }
 8 
 9     func controllerDidChangeUnitsNotation(controller: SettingsViewCo\
10 ntroller) {
11         dayViewController.reloadData()
12         weekViewController.reloadData()
13     }
14 
15     func controllerDidChangeTemperatureNotation(controller: Settings\
16 ViewController) {
17         dayViewController.reloadData()
18         weekViewController.reloadData()
19     }
20 
21 }

Time to Write Some Code

That’s all you need to know about Cloudy for now. In the next chapter, 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.

If you want to run Cloudy, you need to add your Dark Sky API key to Configuration.swift. Signing up for a developer account is free and it only takes a minute.

Configuration.swift

 1 struct API {
 2 
 3     static let APIKey = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
 4     static let BaseURL = URL(string: "https://api.darksky.net/foreca\
 5 st/")!
 6 
 7     static var AuthenticatedBaseURL: URL {
 8         return BaseURL.appendingPathComponent(APIKey)
 9     }
10     
11 }