The code below demonstrates how to extend Encodable protocol to get parameters for the HTTP GET request directly out of the swift structure. Using this way there is no need to manually create an error prone dictionary of parameters. This code also shows one of the ways how to handle HTTP errors when making data task requests using URLSession dataTask
publisher.
import UIKit
import Combine
import PlaygroundSupport
extension Encodable {
var dictionary: [String: Any]? {
guard let data = try? JSONEncoder().encode(self) else {
return nil
}
return (try? JSONSerialization.jsonObject(with: data, options: .allowFragments)).flatMap { $0 as? [String: Any] }
}
var queryItems: [URLQueryItem]? {
dictionary?.map { URLQueryItem(name: $0.key, value: String(describing: $0.value)) }
}
}
struct WeatherQueryParams: Encodable {
let lat: Int
let lon: Int
let appid: String
}
struct WeatherParameters: Decodable {
let temp: Double
}
struct WeatherResponse: Decodable {
let main: WeatherParameters
static var placeholder: WeatherResponse {
return WeatherResponse(main: WeatherParameters(temp: 0.0))
}
}
enum WeatherAPIError: Error {
case invalidRequest
case invalidResponse(String)
case noError
init(data: Data?, response: URLResponse) {
guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else {
// TODO: implement parsing of the error from JSON response
self = WeatherAPIError.invalidResponse("\(String(describing: data))")
return
}
self = .noError
}
init(urlError: URLError) {
// TODO: implement mapping URLError to WeatherAPIError values
self = WeatherAPIError.invalidResponse("\(urlError.localizedDescription)")
}
init(jsonDecodingError: Error) {
self = .invalidResponse(jsonDecodingError.localizedDescription)
}
}
func createRequest() -> Result<URLRequest, Error> {
let baseURL = "https://samples.openweathermap.org"
guard var urlComponents = URLComponents(string: baseURL) else {
return .failure(WeatherAPIError.invalidRequest)
}
urlComponents.path = "/data/2.5/weather"
// Please note how a structure is converted into query parameters. This way is safer than using a Dictionary.
urlComponents.queryItems = WeatherQueryParams(lat: 35, lon: 139, appid: "b6907d289e10d714a6e88b30761fae22").queryItems
guard let url = urlComponents.url else {
return .failure(WeatherAPIError.invalidRequest)
}
return .success(URLRequest(url: url))
}
func sendRequest(urlRequest: URLRequest) -> AnyPublisher<WeatherResponse, Error> {
URLSession.shared.dataTaskPublisher(for: urlRequest)
.catch{ err in
// URLError handling
return Fail<(data: Data, response: URLResponse), Error>(error: WeatherAPIError(urlError: err)).eraseToAnyPublisher()
}
.tryMap { data, response -> Data in
// HTTP error codes handling if any
let apiError = WeatherAPIError(data: data, response: response)
guard case .noError = apiError else {
throw apiError
}
return data
}
.decode(type: WeatherResponse.self, decoder: JSONDecoder())
.catch { (jsonDecodingError) -> AnyPublisher<WeatherResponse, Error> in
// JSON decoding errors handling
return Fail<WeatherResponse, Error>(error: WeatherAPIError(jsonDecodingError: jsonDecodingError)).eraseToAnyPublisher()
}
.eraseToAnyPublisher()
}
func checkWeather() -> AnyPublisher<WeatherResponse, Error> {
let result = createRequest()
switch result {
case .failure(let error):
return Fail<WeatherResponse, Error>(error: error).eraseToAnyPublisher()
case .success(let urlRequest):
return sendRequest(urlRequest: urlRequest).eraseToAnyPublisher()
}
}
var cancellableSet: Set<AnyCancellable> = []
checkWeather().sink(receiveCompletion: { (completion) in
print(completion)
}) { (weather) in
print(weather)
}
.store(in: &cancellableSet)
PlaygroundPage.current.needsIndefiniteExecution = true