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.