Building a Weather Application From Scratch

Implementing the Day View Model

Implementing the Day View Model

Everything's in place to populate the day view controller. Open DayViewModel.swift. Press the Option key and select DayViewController.swift to open it in the Assistant Editor on the right. Implementing the DayViewModel struct is surprisingly straightforward. We first need to inspect the DayViewController class and figure out what type of data the view model needs to provide to populate its user interface.

Let's start with the date label. It displays the date of the weather data. It's the responsibility of the view model to take the raw value of the Dark Sky response and transform it into something the date label can display, that is, a string.

The CurrentWeatherConditions struct has a time property of type Date. We use the Date instance to create a string that can be displayed by the date label. For that to work, we need a date formatter. Define a property, dateFormatter, of type DateFormatter. We instantiate a DateFormatter instance and assign it to the dateFormatter property.

import Foundation

struct DayViewModel {

    // MARK: - Properties

    let weatherData: CurrentWeatherConditions

    // MARK: -

    private let dateFormatter = DateFormatter()

}

The view controller doesn't need access to the dateFormatter property, which means we can declare it as private.

We define a computed property, date, of type String. In the getter of the computed property, we set the dateFormat property of the date formatter and ask it to convert the value stored in the time property of weatherData to a string. That's it.

import Foundation

struct DayViewModel {

    // MARK: - Properties

    let weatherData: CurrentWeatherConditions

    // MARK: -

    private let dateFormatter = DateFormatter()

    // MARK: -

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

        // Convert Date to String
        return dateFormatter.string(from: weatherData.time)
    }

}

We can repeat these steps to populate the time label. The time label displays the time of the weather data. We define a computed property, time, of type String. In the getter of the computed property, we set the dateFormat property of the date formatter and ask it to convert the value stored in the time property of weatherData to a string.

import Foundation

struct DayViewModel {

    // MARK: - Properties

    let weatherData: CurrentWeatherConditions

    // MARK: -

    private let dateFormatter = DateFormatter()

    // MARK: -

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

        // Convert Date to String
        return dateFormatter.string(from: weatherData.time)
    }

    var time: String {
        // Configure Date Formatter
        dateFormatter.dateFormat = "hh:mm a"

        // Convert Date to String
        return dateFormatter.string(from: weatherData.time)
    }

}

Populating the description label is trivial. We define a computed property, summary, of type String. The getter returns the value stored in the summary property of weatherData. It doesn't get any simpler than this.

var summary: String {
    return weatherData.summary
}

The computed properties for the temperature and wind speed labels are almost as easy to implement. To populate the temperature label, we define a computed property, temperature, of type String. The value stored in the temperature property of weatherData is of type Double and is in degrees Fahrenheit. We format the value and convert it to a string.

var temperature: String {
    return String(format: "%.1f °F", weatherData.temperature)
}

To populate the wind speed label, we define a computed property, windSpeed, of type String. The value stored in the windSpeed property of weatherData is of type Double and is in miles per hour. We format the value and convert it to a string.

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

Setting the image property of the image view is more complex. The first step is easy, but it requires a bit of work. The icon property of weatherData is of type String. Its value is one of a fixed set of possible values. This is documented on the Dark Sky website.

Dark Sky Documentation

The documentation explains that the Dark Sky API returns one of ten possible values. Rainstorm only displays the weather conditions. We don't make a distinction between day and night with the exception of clear-day and clear-night.

The icons I plan to use were created by Adam Whitcroft. While I won't cover the creation of an asset catalog in detail in this episode, I want to show you that it isn't difficult. Download the archive from Adam's website and unzip it. Select the folder named SVG and open Sun.svg in your image editor of choice. I use Sketch. For each asset, I create three versions, 1x, 2x, and 3x. Depending on the deployment target of the project, it isn't necessary to create a 1x version since every modern iOS device has a retina display.

Downloading Assets

Exporting Assets

Return to Xcode and open Assets.xcassets. Create a folder and name it Weather Icons. A bit of structure never hurts. Create a new image set and name it clear-day. Add the images you created a moment ago.

Creating a New Image Set

Later in this episode, we apply a tint to the image we display in the image view of the day view controller. To make this possible, we need to modify an attribute of the image set. With the image set selected, open the Attributes Inspector and set Render As to Template Image.

Image Set Attributes

The idea is simple. By setting the rendering mode of the image set to Template Image, Xcode creates a template from the image by inspecting the image data. Color information is ignored if you set the rendering mode of an image set to Template Image.

If you don't want to spend the time creating the image sets or you're not comfortable using an image editor such as Sketch, then I suggest you download the finished project of this episode. Unzip the archive, locate the Resources folder, and add Assets.xcassets to your project. That saves you a few minutes of work.

Later in this series, we populate the week view controller with weather data. The table view cells of the week view controller also display an icon. To avoid code duplication, we implement a helper method on UIImage. The helper method makes it easier to load an image from the asset catalog based on the response of the Dark Sky API. Let me show you how this works.

Create a new Swift file in the Extensions group and name it UIImage.swift. Add an import statement for the UIKit framework and create an extension for UIImage. Define a class method, imageForIcon(with:), which returns an optional UIImage instance.

import UIKit

extension UIImage {

    class func imageForIcon(with name: String) -> UIImage? {

    }

}

We use a switch statement with two cases and a default clause. If the value of the name parameter is equal to clear-day, clear-night, fog, rain, snow, sleet, or wind, we instantiate a UIImage instance by loading an image with that name.

import UIKit

extension UIImage {

    class func imageForIcon(with name: String) -> UIImage? {
        switch name {
        case "clear-day",
             "clear-night",
             "fog",
             "rain",
             "snow",
             "sleet",
             "wind":
            return UIImage(named: name)
        }
    }

}

If the name is equal to cloudy, partly-cloudy-day, or partly-cloudy-night, we instantiate a UIImage instance by loading an image with name cloudy.

import UIKit

extension UIImage {

    class func imageForIcon(with name: String) -> UIImage? {
        switch name {
        case "clear-day",
             "clear-night",
             "fog",
             "rain",
             "snow",
             "sleet",
             "wind":
            return UIImage(named: name)
        case "cloudy",
             "partly-cloudy-day",
             "partly-cloudy-night":
            return UIImage(named: "cloudy")
        }
    }

}

Because switch statements need to be exhaustive, we add a default clause in which we instantiate and return a UIImage instance by loading an image with name clear-day. We could also return nil in that scenario.

import UIKit

extension UIImage {

    class func imageForIcon(with name: String) -> UIImage? {
        switch name {
        case "clear-day",
             "clear-night",
             "fog",
             "rain",
             "snow",
             "sleet",
             "wind":
            return UIImage(named: name)
        case "cloudy",
             "partly-cloudy-day",
             "partly-cloudy-night":
            return UIImage(named: "cloudy")
        default:
            return UIImage(named: "clear-day")
        }
    }

}

With the class method on UIImage in place, we can finalize the implementation of the DayViewModel struct. To take advantage of the class method on UIImage, we first need to add an import statement for the UIKit framework at the top.

import UIKit

struct DayViewModel {

    // MARK: - Properties

    let weatherData: CurrentWeatherConditions

    ...

}

We implement a computed property, image, of type UIImage?. The implementation is short and simple thanks to the imageForIcon(with:) method. We pass the value stored in the icon property of weatherData to the class method.

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

Populating the Day View Controller

Open DayViewController.swift and navigate to the setupViewModel(with:) method. The implementation isn't difficult thanks to the DayViewModel struct. We ask the view model for the data the day view controller needs to populate its user interface elements.

// MARK: - Helper Methods

private func setupViewModel(with viewModel: DayViewModel) {
    // Configure Labels
    dateLabel.text = viewModel.date
    timeLabel.text = viewModel.time
    windSpeedLabel.text = viewModel.windSpeed
    temperatureLabel.text = viewModel.temperature
    descriptionLabel.text = viewModel.summary

    // Configure Icon Image View
    iconImageView.image = viewModel.image
}

This is one of the niceties of the Model-View-ViewModel pattern. The view controller remains focused. Its only responsibilities are populating the user interface and responding to user interaction. The nitty gritty details are handled by its view model. Because the view model doesn't have a link to the user interface of the view controller, it's easy to write unit tests for the view model. That's the focus of the next episode.

Build and Run

Run the application so see the result. You may notice a few issues. It takes a few seconds for the application to show the current weather conditions. It may even be possible that the application doesn't display any weather data. What's happening?

Xcode gives us a hint. Open the Issue Navigator on the left and select Runtime. We have one runtime issue. Xcode notifies us that we update the user interface from a background thread. The user interface of an application should always be updated from the main thread. Updating the user interface from a background thread results in unexpected behavior as you can see in this example.

Runtime Issue

We can verify this by adding a breakpoint to the setupViewModel(with:) method. Run the application again and open the Debug Navigator on the left.

Runtime Issue

Notice that the labels are populated on a background thread. How is that possible? The stack trace shows us what's happening. The completion handler of the data task the RootViewModel instance uses to fetch weather data from the Dark Sky API is executed on a background thread. That isn't surprising. It's expected behavior. The problem is that we invoke the closure stored in the didFetchWeatherData property in the completion handler of the data task.

The issue is easy to resolve. To keep the implementation of the fetchWeatherData() method simple, we handle the response of the Dark Sky API request on the main thread. This isn't as complicated as it sounds. We ask the DispatchQueue class for a reference to the main queue. The main queue is the queue that is associated with the main thread. We hand it a block of work by passing a closure to its async(group:qos:flags:execute:) method. In the closure, we handle the response of the Dark Sky API request and invoke the closure stored in the didFetchWeatherData property.

private func fetchWeatherData() {
    // Initialize Weather Request
    let weatherRequest = WeatherRequest(baseUrl: WeatherService.authenticatedBaseUrl, location: Defaults.location)

    // Create Data Task
    URLSession.shared.dataTask(with: weatherRequest.url) { [weak self] (data, response, error) in
        if let response = response as? HTTPURLResponse {
            print("Status Code: \(response.statusCode)")
        }

        DispatchQueue.main.async {
            if let error = error {
                print("Unable to Fetch Weather Data \(error)")

                self?.didFetchWeatherData?(nil, .noWeatherDataAvailable)
            } else if let data = data {
                // Initialize JSON Decoder
                let decoder = JSONDecoder()

                do {
                    // Decode JSON Response
                    let darkSkyResponse = try decoder.decode(DarkSkyResponse.self, from: data)

                    // Invoke Completion Handler
                    self?.didFetchWeatherData?(darkSkyResponse, nil)
                } catch {
                    print("Unable to Decode JSON Response \(error)")

                    // Invoke Completion Handler
                    self?.didFetchWeatherData?(nil, .noWeatherDataAvailable)
                }
            } else {
                self?.didFetchWeatherData?(nil, .noWeatherDataAvailable)
            }
        }
        }.resume()
}

Run the application one more time. The runtime issue should disappear and the user interface should be updated moments after the application receives a response from the Dark Sky API.

Dates and Timestamps

If you take a good look at the user interface, you may notice that there's another issue. The year the date label displays is in the distant future. The Dark Sky API sends the application a timestamp, which the JSONDecoder instance converts to a Date instance.

The problem is that the Dark Sky API sends a Unix timestamp while the JSONDecoder instance expects a different type of timestamp. A Unix timestamp defines a date as the number of seconds that have passed since January 1, 1970. The JSONDecoder class expects a timestamp that defines a date as the number of seconds that have passed since January 1, 2001. That explains the difference we're seeing in the user interface.

The solution is simple, though. We need to configure the JSONDecoder instance. Revisit the RootViewModel class and set the date decoding strategy of the JSONDecoder instance to secondsSince1970. By setting the date decoding strategy to secondsSince1970, we notify the JSONDecoder instance that any timestamps it encounters are Unix timestamps.

// Initialize JSON Decoder
let decoder = JSONDecoder()

// Configure JSON Decoder
decoder.dateDecodingStrategy = .secondsSince1970

With this simple change, the issue should be resolved. Run the application to see the result.

Adding Polish

It takes a few moments before the application can display weather data to the user because the application needs to perform a request. The labels of the application display dummy text for as long as there's no weather data to display. This isn't pretty. We can do better. Let's display an activity indicator view for as long as the request is in flight.

Define an outlet, activityIndicatorView, of type UIActivityIndicatorView. We configure the UIActivityIndicatorView instance in a didSet property observer. We instruct it to start animating and it should hide itself from the moment it stops animating.

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

Open Main.storyboard and add a UIActivityIndicatorView instance to the view of the DayViewController class. We don't need to set the size of the activity indicator view, but we need to add layout constraints to define its position. We position the activity indicator view at the center of the image view of the day view controller.

Add Activity Indicator View

Open the Connections Inspector on the right, select the day view controller, and connect the activity indicator view with its outlet.

Add Activity Indicator View

The labels and image view of the day view controller should be hidden if they don't have any weather data to display. This is easy to do with an outlet collection. Open DayViewController.swift and define an outlet collection, weatherDataViews, of type [UIView]. In a didSet property observer, we hide the members of the collection.

@IBOutlet var weatherDataViews: [UIView]! {
    didSet {
        for view in weatherDataViews {
            view.isHidden = true
        }
    }
}

In the setupViewModel(with:) method, we instruct the activity indicator view to stop animating, we populate the labels and image view with weather data, and we show the members of the outlet collection.

// MARK: - Helper Methods

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

    // Configure Labels
    dateLabel.text = viewModel.date
    timeLabel.text = viewModel.time
    windSpeedLabel.text = viewModel.windSpeed
    temperatureLabel.text = viewModel.temperature
    descriptionLabel.text = viewModel.summary

    // Configure Icon Image View
    iconImageView.image = viewModel.image

    // Show Weather Data Views
    for view in weatherDataViews {
        view.isHidden = false
    }
}

Revisit Main.storyboard and connect the weatherDataViews outlet with the labels and the image view of the day view controller. Outlet collections can be very convenient to avoid code duplication.

Outlet Collection in Interface Builder

We could have hidden the labels and image view in the storyboard. That would work just fine. I prefer to hide the labels and image view in code to make it explicit that these views are initially hidden. It's a personal choice. I like it because it makes it easier to understand what's happening without having to browse the storyboard.

Before running the application one more time, navigate to the didSet property observer of the iconImageView property. Earlier in this episode, I mentioned that I planned to tint the image displayed by the image view of the day view controller. This is easy to do.

Revisit Styles.swift and define a static computed property, baseTintColor, of type UIColor. In the getter of the computed property, we return the value stored in the base static property.

import UIKit

extension UIColor {

    enum Rainstorm {

        private static let base: UIColor = UIColor(red: 0.31, green: 0.72, blue: 0.83, alpha: 1.0)

        static var baseTextColor: UIColor {
            return base
        }

        static var baseTintColor: UIColor {
            return base
        }

        static var baseBackgroundColor: UIColor {
            return base
        }

    }

}

...

We also define a background color for the view of the day view controller, a light, gray color. We name the static property lightBackgroundColor.

import UIKit

extension UIColor {

    enum Rainstorm {

        private static let base: UIColor = UIColor(red: 0.31, green: 0.72, blue: 0.83, alpha: 1.0)

        static var baseTextColor: UIColor {
            return base
        }

        static var baseTintColor: UIColor {
            return base
        }

        static var baseBackgroundColor: UIColor {
            return base
        }

        static let lightBackgroundColor: UIColor = UIColor(red: 0.975, green: 0.975, blue: 0.975, alpha: 1.0)

    }

}

...

Revisit DayViewController.swift and set the tintColor property of the image view of the day view controller in its didSet property observer.

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

Navigate to the setupView() method and update the background color of the day view controller's view. That's it. Run the application to see the result.

// MARK: - View Methods

private func setupView() {
    // Configure View
    view.backgroundColor = UIColor.Rainstorm.lightBackgroundColor
}

What's Next?

Even though we did quite a bit of work in this episode, there wasn't anything that was terribly complicated. If you're unfamiliar with threads and queues, then the runtime issue we ran into might have been a bit difficult to understand.

In the next episode, it's time we put the implementation of the DayViewModel struct to the test by writing a handful of unit tests.

Next Episode "Writing Unit Tests for the Day View Model"

Stop Writing Swift That Sucks

Download the Swift Patterns I Swear By