It is time to focus on one of the core aspects of the weather application we are building, fetching and displaying weather data. Thunderstorm displays placeholder data for the time being. That is something we change in this and the next episodes.

Exploring the API Response

Before we fetch weather data from the Clear Sky API, we need to define the model for the API response. Let's take a look at the API response first.

The API response defines two fields we are interested in, currently and daily. The value of the currently field is an object that encapsulate the current weather conditions. The value of the daily field is an object with three fields. The field we are interested in is the data field. The value of the data field is an array of objects with each object encapsulating the weather conditions for a given day.

Our task is to translate the API response to a model the weather application can use to display the current weather conditions and forecast. Let's define that model now.

Defining the Model

Add a Swift file to the Models group and name it WeatherData.swift. Declare a struct with name WeatherData that conforms to the Decodable protocol.

import Foundation

struct WeatherData: Decodable {

}

We need to declare two more models, one for the current weather conditions and one for the weather conditions for a given day. Add an extension for the WeatherData struct and declare a nested struct with name CurrentConditions that conforms to the Decodable protocol.

extension WeatherData {

    struct CurrentConditions: Decodable {

	}

}

The CurrentConditions struct defines five properties, time of type Date, icon of type String, summary of type String, windSpeed of type Float, and temperature of type Float.

extension WeatherData {

    struct CurrentConditions: Decodable {

        // MARK: - Properties

        let time: Date
        let icon: String
        let summary: String
        let windSpeed: Float
        let temperature: Float

    }

}

Add another extension for the WeatherData struct and declare a nested struct with name DayConditions that conforms to the Decodable protocol.

extension WeatherData {

    struct DayConditions: Decodable {
		
    }

}

The DayConditions struct defines six properties, time of type Date, icon of type String, summary of type String, windSpeed of type Float, temperatureHigh of type Float, and temperatureLow of type Float.

extension WeatherData {

    struct DayConditions: Decodable {

        // MARK: - Properties

        let time: Date
        let icon: String
        let summary: String
        let windSpeed: Float
        let temperatureHigh: Float
        let temperatureLow: Float

    }

}

Let's revisit the WeatherData struct. Declare two properties, currently of type CurrentConditions and forecast of type [DayConditions].

import Foundation

struct WeatherData: Decodable {

    // MARK: - Properties

    let currently: CurrentConditions
    let forecast: [DayConditions]

}

extension WeatherData {

    struct CurrentConditions: Decodable {

		...

    }

}

extension WeatherData {

    struct DayConditions: Decodable {

		...

    }

}

Decoding the API Response

We need to make a few minor changes to decode a WeatherData object from the Clear Sky API response. Declare a private, nested enum with name RootCodingKeys. The raw values of the RootCodingKeys enum are of type String and the RootCodingKeys enum conforms to the CodingKey protocol. It defines two cases, currently and daily.

import Foundation

struct WeatherData: Decodable {

    // MARK: - Types

    private enum RootCodingKeys: String, CodingKey {
        case currently, daily
    }

    // MARK: - Properties

    let currently: CurrentConditions
    let forecast: [DayConditions]

}

We declare another private, nested enum with name DailyCodingKeys. The raw values of the DailyCodingKeys enum are of type String and the DailyCodingKeys enum conforms to the CodingKey protocol. It defines one case, data.

import Foundation

struct WeatherData: Decodable {

    // MARK: - Types

    private enum RootCodingKeys: String, CodingKey {
        case currently, daily
    }

    private enum DailyCodingKeys: String, CodingKey {
        case data
    }

    // MARK: - Properties

    let currently: CurrentConditions
    let forecast: [DayConditions]

}

Why we declare these enums becomes clear when we implement the initializer of the Decodable protocol. The initializer accepts a Decoder object as its only argument. In the body of the initializer, we decode a WeatherData object from an API response. The initializer is required, but a default implementation is provided so we are not required to implement it. That said, we implement the initializer to flatten the response the API returns. Let me show you what that means.

import Foundation

struct WeatherData: Decodable {

    // MARK: - Types

    private enum RootCodingKeys: String, CodingKey {
        case currently, daily
    }

    private enum DailyCodingKeys: String, CodingKey {
        case data
    }

    // MARK: - Properties

    let currently: CurrentConditions
    let forecast: [DayConditions]

    // MARK: - Initialization

    init(from decoder: Decoder) throws {
    	
    }

}

We ask the Decoder object for a keyed decoding container by invoking the container(keyedBy:) method, passing in the RootCodingKeys enum as an argument. We use the keyed decoding container to decode a CurrentConditions object for the currently key, assigning the result to the currently property.

// MARK: - Initialization

init(from decoder: Decoder) throws {
    let container = try decoder.container(
        keyedBy: RootCodingKeys.self
    )

    currently = try container.decode(
        CurrentConditions.self,
        forKey: .currently
    )
}

We use a similar approach to decode the value for the forecast property. We ask the keyed decoding container for a nested keyed decoding container by invoking the nestedContainer(keyedBy:forKey:) method, passing in the DailyCodingKeys enum as the first argument and the daily key as the second argument.

// MARK: - Initialization

init(from decoder: Decoder) throws {
    let container = try decoder.container(
        keyedBy: RootCodingKeys.self
    )

    currently = try container.decode(
        CurrentConditions.self,
        forKey: .currently
    )

    let forecastContainer = try container.nestedContainer(
        keyedBy: DailyCodingKeys.self,
        forKey: .daily
    )
}

We use the nested keyed decoding container to decode an array of DayConditions objects for the data key, assigning the result to the forecast property.

// MARK: - Initialization

init(from decoder: Decoder) throws {
    let container = try decoder.container(
        keyedBy: RootCodingKeys.self
    )

    currently = try container.decode(
        CurrentConditions.self,
        forKey: .currently
    )

    let forecastContainer = try container.nestedContainer(
        keyedBy: DailyCodingKeys.self,
        forKey: .daily
    )

    forecast = try forecastContainer.decode(
        [DayConditions].self,
        forKey: .data
    )
}

Decoding Preview Data

The previews also need to display weather data, but the weather data for the previews shouldn't be fetched from the Clear Sky API. It is faster and more reliable to include the weather data for the previews in the project. We add a response from the Clear Sky API to the Preview Content group. The file is included in the finished project of this episode.

Add a Swift file to the Preview Content group and name it WeatherData+Preview.swift. Add an extension for the WeatherData struct and declare a static, variable property with name Preview of type Self.

import Foundation

extension WeatherData {

    static var preview: Self {
    	
    }

}

We ask the main bundle for the URL for the API response we added to the Preview Content group. The url(forResource:withExtension:) method accepts the name of the file and the extension of the file as arguments. Because the preview data is only used for previews, we force unwrap the optional the url(forResource:withExtension:) method returns.

import Foundation

extension WeatherData {

    static var preview: Self {
        let url = Bundle.main.url(
            forResource: "clearsky",
            withExtension: "json"
        )!
    }

}

The next step should feel familiar. We create a Data object with the URL object and use a JSONDecoder instance to decode the WeatherData object from the API response. Note that we use the try! keyword because the preview data is only used for previews.

import Foundation

extension WeatherData {

    static var preview: Self {
        let url = Bundle.main.url(
            forResource: "clearsky",
            withExtension: "json"
        )!

        let data = try! Data(contentsOf: url)

        return try! JSONDecoder().decode(
            WeatherData.self,
            from: data
        )
    }

}

Creating the Clear Sky Decoder

There is one more change we need to make. We need to set the date decoding strategy of the JSONDecoder instance to secondsSince1970. While we could set the dateDecodingStrategy property every time we decode a Clear Sky response, to avoid bugs and avoid confusion, we define a JSONDecoder subclass instead.

Create a group with name Utilities and add a Swift file to the group with name ClearSkyDecoder.swift. Declare a final class with name ClearSkyDecoder. The superclass of the ClearSkyDecoder class is JSONDecoder.

import Foundation

final class ClearSkyDecoder: JSONDecoder {
	
}

The ClearSkyDecoder class overrides the initializer of its superclass. In the initializer, we set the dateDecodingStrategy property to secondsSince1970.

import Foundation

final class ClearSkyDecoder: JSONDecoder {

    // MARK: - Initialization

    override init() {
        super.init()

        dateDecodingStrategy = .secondsSince1970
    }

}

Creating a JSONDecoder subclass may feel unnecessary, but I find it is helpful to avoid confusion. It keeps the call site clean and it makes it clear that the ClearSkyDecoder class is used for decoding responses of the Clear Sky API.

Revisit WeatherData+Preview.swift and replace the JSONDecoder instance with a ClearSkyDecoder instance.

import Foundation

extension WeatherData {

    static var preview: Self {
        let url = Bundle.main.url(
            forResource: "clearsky",
            withExtension: "json"
        )!

        let data = try! Data(contentsOf: url)

        return try! ClearSkyDecoder().decode(
            WeatherData.self,
            from: data
        )
    }

}

Build the Thunderstorm target and verify that no errors or warnings pop up.

What's Next?

With the WeatherData struct in place, we can focus on fetching weather data from the Clear Sky API. We do that in the next episode.