In this episode, we add the ability to fetch weather data from the Clear Sky API and decode a WeatherData object from the API response. We apply a pattern that should feel familiar by now.
Defining a Protocol
Create a group with name Weather and add a Swift file to the group with name WeatherService.swift. Declare a protocol with name WeatherService. The WeatherService protocol defines the interface the application uses to fetch weather data.
import Foundation
protocol WeatherService {
}
The WeatherService protocol defines a method with name weather(for:). It accepts the location for which to fetch weather data as an argument. The method is asynchronous and throwing, and it returns a WeatherData object.
import Foundation
protocol WeatherService {
// MARK: - Methods
func weather(for location: Location) async throws -> WeatherData
}
Implementing the Weather Client
We define two types that conform to the WeatherService protocol, a type for fetching weather data from the Clear Sky API and a type for providing previews with weather data. Add a Swift file to the Weather group and name it WeatherClient.swift. Declare a final class with name WeatherClient that conforms to the WeatherService protocol.
import Foundation
final class WeatherClient: WeatherService {
}
To conform to the WeatherService protocol, the WeatherClient class needs to implement the weather(for:) method.
import Foundation
final class WeatherClient: WeatherService {
// MARK: - Methods
func weather(for location: Location) async throws -> WeatherData {
}
}
To send the request to the Clear Sky API, we need to create a URLRequest object. Before we do that, we define a private, constant property with name baseURL. We set the property's value to the base URL of the Clear Sky API. Note that we force unwrap the result of the initializer. This is one of the few cases in which I use the exclamation mark in production code. Creating the base URL shouldn't fail hence the exclamation mark.
import Foundation
final class WeatherClient: WeatherService {
// MARK: - Properties
private let baseURL = URL(string: "https://cocoacasts.com/clearsky")!
// MARK: - Methods
func weather(for location: Location) async throws -> WeatherData {
}
}
The request we send to the Clear Sky API is a GET request that accepts three query parameters, the latitude, the longitude, and an API key. We define these query parameters as an array of URLQueryItem objects. Declare a constant with name queryItems of type [URLQueryItem]. We create an array literal that contains the aforementioned query parameters. Note that we pass the latitude and longitude as strings. The value of a URLQueryItem object is always a string hence the conversion of the latitude and longitude to strings.
// MARK: - Methods
func weather(for location: Location) async throws -> WeatherData {
let queryItems: [URLQueryItem] = [
.init(name: "lat", value: "\(location.latitude)"),
.init(name: "long", value: "\(location.longitude)"),
.init(name: "api_key", value: "tnperxfip8renk2hlzcccwetbnesby")
]
}
With the query items defined, we create the URL for the GET request by appending the array of query items to the base URL. We invoke the appending(queryItems:) method on the baseURL property to construct the URL for the GET request.
// MARK: - Methods
func weather(for location: Location) async throws -> WeatherData {
let queryItems: [URLQueryItem] = [
.init(name: "lat", value: "\(location.latitude)"),
.init(name: "long", value: "\(location.longitude)"),
.init(name: "api_key", value: "tnperxfip8renk2hlzcccwetbnesby")
]
let url = baseURL.appending(queryItems: queryItems)
}
We use the URL object to create the URLRequest object for the GET request.
// MARK: - Methods
func weather(for location: Location) async throws -> WeatherData {
let queryItems: [URLQueryItem] = [
.init(name: "lat", value: "\(location.latitude)"),
.init(name: "long", value: "\(location.longitude)"),
.init(name: "api_key", value: "tnperxfip8renk2hlzcccwetbnesby")
]
let url = baseURL.appending(queryItems: queryItems)
let request = URLRequest(url: url)
}
To send the request to the Clear Sky API, we invoke the data(for:) method on the URLSession singleton. The data(for:) method is asynchronous and throwing. It returns a tuple that contains two values, a Data object and a URLResponse object. We are only interested in the Data object.
// MARK: - Methods
func weather(for location: Location) async throws -> WeatherData {
let queryItems: [URLQueryItem] = [
.init(name: "lat", value: "\(location.latitude)"),
.init(name: "long", value: "\(location.longitude)"),
.init(name: "api_key", value: "tnperxfip8renk2hlzcccwetbnesby")
]
let url = baseURL.appending(queryItems: queryItems)
let request = URLRequest(url: url)
let (data, _) = try await URLSession.shared.data(for: request)
}
To decode a WeatherData object from the API response, we create a ClearSkyDecoder instance and invoke its decode(_:from:) method, passing in the WeatherData struct as the first argument and the Data object returned by the data(for:) method as the second argument. We return the WeatherData object from the weather(for:) method.
// MARK: - Methods
func weather(for location: Location) async throws -> WeatherData {
let queryItems: [URLQueryItem] = [
.init(name: "lat", value: "\(location.latitude)"),
.init(name: "long", value: "\(location.longitude)"),
.init(name: "api_key", value: "tnperxfip8renk2hlzcccwetbnesby")
]
let url = baseURL.appending(queryItems: queryItems)
let request = URLRequest(url: url)
let (data, _) = try await URLSession.shared.data(for: request)
return try ClearSkyDecoder().decode(
WeatherData.self,
from: data
)
}
Implementing the Weather Preview Client
Remember that we also need a weather service for previews. Add a Swift file to the Weather group and name it WeatherPreviewClient.swift. Declare a struct with name WeatherPreviewClient that conforms to the WeatherService protocol.
import Foundation
struct WeatherPreviewClient: WeatherService {
}
To conform to the WeatherService protocol, the WeatherPreviewClient struct needs to implement the weather(for:) method. The implementation couldn't be simpler. We return the value of the static preview property of the WeatherData struct we implemented in the previous episode.
import Foundation
struct WeatherPreviewClient: WeatherService {
func weather(for location: Location) async throws -> WeatherData {
.preview
}
}
What's Next?
The application now has the ability to fetch weather data from the Clear Sky API. Before we fetch weather data, we slightly tweak the implementation of the WeatherClient class. You may have noticed that we hard-coded the base URL and API key of the Clear Sky API in the WeatherClient class. I'm not a fan of hard-coding configuration details. In the next episode, I show you an alternative approach.