In the previous episode, we parsed the JSON response of the Dark Sky API. It's time to integrate the DarkSkyResponse struct into the Rainstorm project. Remember that we didn't handle any errors in the playground. That's also something we tackle in this episode.
Integrating the DarkSkyResponse Struct
We start by adding the DarkSkyResponse struct of the playground to the Rainstorm project. Create a new file in the Models group of the Rainstorm project by choosing the Swift File template from the iOS section. Name the file DarkSkyResponse.swift.


Copy the contents of DarkSkyResponse.swift from the playground and paste it into the file we just created in the Rainstorm project. Because DarkSkyResponse.swift is a member of the Rainstorm target, we no longer need to declare the DarkSkyResponse struct and its properties publicly. We can remove the public keywords in DarkSkyResponse.swift.
import Foundation
struct DarkSkyResponse: Codable {
struct Conditions: Codable {
let time: Date
let icon: String
let summary: String
let windSpeed: Double
let temperature: Double
}
struct Daily: Codable {
let data: [Conditions]
struct Conditions: Codable {
let time: Date
let icon: String
let windSpeed: Double
let temperatureMin: Double
let temperatureMax: Double
}
}
let latitude: Double
let longitude: Double
let daily: Daily
let currently: Conditions
}
That's the first step. The next step is integrating the DarkSkyResponse struct into the RootViewModel class.
We start by updating the type alias at the top of RootViewModel.swift. We won't be passing the Data instance to the completion handler of the RootViewModel class. We replace the optional Data instance with an optional DarkSkyResponse instance.
import Foundation
class RootViewModel {
// MARK: - Type Aliases
typealias DidFetchWeatherDataCompletion = (DarkSkyResponse?, Error?) -> Void
...
}
This also means we need to update the implementation of the fetchWeatherData() method. If the request we send to the Dark Sky API returns a valid Data instance, we use the data to instantiate a DarkSkyResponse instance. We initialize an instance of the JSONDecoder class and use it in a do-catch statement to decode the contents of the Data instance. We pass the DarkSkyResponse instance to the completion handler as well as any errors that are thrown in the catch clause.
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 error = error {
self?.didFetchWeatherData?(nil, error)
} 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, error)
}
} else {
self?.didFetchWeatherData?(nil, nil)
}
}.resume()
}
Handling Errors
Even though this may look much better than passing the Data instance to the RootViewController class via the completion handler, there are a few problems we need to address. The current implementation passes any errors that are thrown to the RootViewController class via the completion handler. Is that what we want? Does the RootViewController class know or understand how to handle these errors?
Swift has great support for error handling, but it's clear that many developers still struggle with error handling. Handling errors isn't a lot of fun, but it's necessary if your goal is building a robust application. There are no hard rules for handling errors and that's why it's a confusing topic for many developers.
I'd like to give you a few tips that can help you stay on top of the errors that are thrown in a project. The first tip is simple. Don't propagate errors that make no sense without context. What does that mean? Let me illustrate this with an example. We could pass the errors thrown by the URLSession API and the JSONDecoder class to the completion handler. That may even seem like the solution that makes the most sense. This isn't a good idea, though. Why is that?
It means that the RootViewController class needs to know about the URLSession API and the JSONDecoder class. In other words, the RootViewController class needs to know that the weather data is fetched by sending a request to the Dark Sky API. That's an implementation detail the RootViewController class shouldn't need to know about. Remember that the view model of the RootViewController class is responsible for performing the request to the Dark Sky API and the RootViewController class shouldn't need to know about this implementation detail.
The same is true for handling the JSON response. It may make sense to notify the RootViewController class if the application is unable to decode the Dark Sky response. The root view controller is only interested in one question "Does the application have weather data to show to the user?"
And that's the true challenge developers struggle with. They inspect the errors that could be thrown at runtime and they try to figure out how they can communicate these errors to the user. This leads to frustration and it very often results in obscure error messages or, even worse, no error handling.
The second tip I'd like to share with you relates to the user experience. We're building applications for human beings. Right? We inspect the errors that could be thrown at runtime and map them to scenarios the user may encounter. Let's take another simple example. Let's assume the user's device isn't connected to the web, which means the application is unable to fetch weather data. The URLSession API throws an error as a result. Instead of showing the user an alert with a cryptic message, we simply inform the user that their device isn't connected to the web. It's that simple. We could make the message of the alert a bit more generic by notifying the user that the application is unable to fetch weather data, asking them to make sure their device is connected to the web.
There are often a bunch of errors that make no sense to the user. For example, if the JSON response of the Dark Sky API couldn't be parsed because of a missing key, then we need to handle that error gracefully by informing the user that a technical issue prevented the application from fetching weather data. It's true that this doesn't give the user much information, but it isn't useful to display an error code or explain to the user what JSON is and why decoding of the Dark Sky response failed.
Notifying the User
To understand which errors the RootViewController class needs to respond to, we first need to step into the user's shoes. When the user opens the application, they expect to see weather data. Are they interested in where that data comes from? No. Do they care if the application is unable to decode the Dark Sky response? No. The user is only interested in the presence or absence of weather data.
Remember what I said earlier, it isn't useful or even desirable to propagate errors that make no sense without context. The solution I have in mind is simple. The RootViewModel class inspects the errors that are thrown by the URLSession API and the JSONDecoder class, and it transforms them to an error the RootViewController class can understand. At the top of RootViewModel.swift, we define an enum, WeatherDataError, that conforms to the Error protocol.
import Foundation
class RootViewModel {
// MARK: - Types
enum WeatherDataError: Error {
}
...
}
We define one case, noWeatherDataAvailable. While it's tempting to be more specific by defining multiple cases, remember that the error needs to make sense to the RootViewController class. It doesn't know about the URLSession API or the JSONDecoder class. In other words, it isn't helpful to inform the RootViewController class that the application isn't able to decode the Dark Sky response.
import Foundation
class RootViewModel {
// MARK: - Types
enum WeatherDataError: Error {
case noWeatherDataAvailable
}
...
}
We can now update the type alias of the completion handler, replacing the optional Error instance with an optional WeatherDataError instance.
import Foundation
class RootViewModel {
// MARK: - Types
enum WeatherDataError: Error {
case noWeatherDataAvailable
}
// MARK: - Type Aliases
typealias DidFetchWeatherDataCompletion = (DarkSkyResponse?, WeatherDataError?) -> Void
...
}
Updating the fetchWeatherData() method is straightforward. We no longer pass the errors of the URLSession API and the JSONDecoder class to the completion handler. Instead we pass a WeatherDataError instance to the completion handler if anything goes haywire. We also print the error to the console if the Dark Sky request fails.
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 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()
}
Whenever an error is thrown you probably ask yourself "What should I do?" or "How should the application handle the error?". Answering that question is usually the most difficult aspect of error handling.
The truth is that there's very little we can do if the application isn't able to fetch weather data from the Dark Sky API. If decoding the JSON response fails, then we made a mistake we might be able to fix by adding unit tests. If the request to the Dark Sky API fails, the application might be using an expired API key or it exceeded the API's rate limit. This isn't something we can fix in the application. Once the application is deployed to the App Store, there's very little, if anything, we can do to fix this type of errors.
There's another type of error handling we could implement in Rainstorm. The application is highly dependent on the Dark Sky API and we need to make sure that the errors I described a moment ago are being tracked. If a request to the Dark Sky API fails, I, the developer, want to know about that. I don't want to find out from one of my users that the application has become unusable. There are several options to choose from. You can log any errors that are thrown to a remote backend or you can integrate a solution such as New Relic into your application.
But there's a more important solution you can add to catch this type of errors early, a test suite. That's something we cover later in this series.
The RootViewModel class passes an error to the RootViewController class if something goes wrong, but we still need to notify the user. That's easy to do. Open RootViewController.swift and navigate to the setupViewModel(with:) method. In the closure we assign to the didFetchWeatherData property of the view model, we safely unwrap the parameters of the closure. If the WeatherDataError instance isn't equal to nil or if the DarkSkyResponse instance is equal to nil, we notify the user by presenting an alert. Let's implement a helper method for that purpose.
private func setupViewModel(with viewModel: RootViewModel) {
// Configure View Model
viewModel.didFetchWeatherData = { (data, error) in
if let error = error {
print("Unable to Fetch Weather Data (\(error)")
} else if let data = data {
print(data)
}
}
}
To add some flexibility, we define a private enum at the top of RootViewController.swift, AlertType, that defines the type of alert we want to present to the user.
import UIKit
class RootViewController: UIViewController {
// MARK: - Types
private enum AlertType {
case noWeatherDataAvailable
}
...
}
We define a private helper method, presentAlert(of:), that accepts an instance of the AlertType enum.
private func presentAlert(of alertType: AlertType) {
}
We keep the implementation simple for now. We define two constants, title and message, and use a switch statement to assign a value to each constant, based on the value of alertType. This implementation makes it easy to present different alert types without adding more complexity.
private func presentAlert(of alertType: AlertType) {
// Helpers
let title: String
let message: String
switch alertType {
case .noWeatherDataAvailable:
title = "Unable to Fetch Weather Data"
message = "The application is unable to fetch weather data. Please make sure your device is connected over Wi-Fi or cellular."
}
}
We instantiate an instance of the UIAlertController class, passing in the title and message constants. We set the style of the UIAlertController instance to alert.
private func presentAlert(of alertType: AlertType) {
// Helpers
let title: String
let message: String
switch alertType {
case .noWeatherDataAvailable:
title = "Unable to Fetch Weather Data"
message = "The application is unable to fetch weather data. Please make sure your device is connected over Wi-Fi or cellular."
}
// Initialize Alert Controller
let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert)
}
We create a UIAlertAction instance to dismiss the alert and add it to the UIAlertController instance by passing it as an argument to the addAction(_:) method.
private func presentAlert(of alertType: AlertType) {
// Helpers
let title: String
let message: String
switch alertType {
case .noWeatherDataAvailable:
title = "Unable to Fetch Weather Data"
message = "The application is unable to fetch weather data. Please make sure your device is connected over Wi-Fi or cellular."
}
// Initialize Alert Controller
let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert)
// Add Cancel Action
let cancelAction = UIAlertAction(title: "OK", style: .cancel, handler: nil)
alertController.addAction(cancelAction)
}
To present the alert controller, we pass the UIAlertController instance to the present(_:animated:completion:) method of the RootViewController class. There's no need to pass a completion handler to this method.
private func presentAlert(of alertType: AlertType) {
// Helpers
let title: String
let message: String
switch alertType {
case .noWeatherDataAvailable:
title = "Unable to Fetch Weather Data"
message = "The application is unable to fetch weather data. Please make sure your device is connected over Wi-Fi or cellular."
}
// Initialize Alert Controller
let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert)
// Add Cancel Action
let cancelAction = UIAlertAction(title: "OK", style: .cancel, handler: nil)
alertController.addAction(cancelAction)
// Present Alert Controller
present(alertController, animated: true)
}
Revisit the setupViewModel(with:) method and invoke presentAlert(of:) if no weather data is available. To avoid a retain cycle, we define a capture list and weakly capture self.
private func setupViewModel(with viewModel: RootViewModel) {
// Configure View Model
viewModel.didFetchWeatherData = { [weak self] (data, error) in
if let error = error {
// Notify User
self?.presentAlert(of: .noWeatherDataAvailable)
} else if let data = data {
print(data)
} else {
// Notify User
self?.presentAlert(of: .noWeatherDataAvailable)
}
}
}
Because we're currently not inspecting the error that is passed to the completion handler, we ignore the result of the optional binding.
private func setupViewModel(with viewModel: RootViewModel) {
// Configure View Model
viewModel.didFetchWeatherData = { [weak self] (data, error) in
if let _ = error {
// Notify User
self?.presentAlert(of: .noWeatherDataAvailable)
} else if let data = data {
print(data)
} else {
// Notify User
self?.presentAlert(of: .noWeatherDataAvailable)
}
}
}
Let's try it out. Run the application with a valid Dark Sky API key. The DarkSkyResponse instance should be printed to the console.
DarkSkyResponse(latitude: 37.335113999999997, longitude: -122.008928, daily: Rainstorm.DarkSkyResponse.Daily(data: [Rainstorm.DarkSkyResponse.Daily.Conditions(time: 2049-07-03 07:00:00 +0000, icon: "partly-cloudy-day", windSpeed: 2.8999999999999999, temperatureMin: 55.780000000000001, temperatureMax: 72.870000000000005), Rainstorm.DarkSkyResponse.Daily.Conditions(time: 2049-07-04 07:00:00 +0000, icon: "partly-cloudy-day", windSpeed: 4.5899999999999999, temperatureMin: 53.509999999999998, temperatureMax: 75.25), Rainstorm.DarkSkyResponse.Daily.Conditions(time: 2049-07-05 07:00:00 +0000, icon: "partly-cloudy-night", windSpeed: 3.3799999999999999, temperatureMin: 52.409999999999997, temperatureMax: 84.079999999999998), Rainstorm.DarkSkyResponse.Daily.Conditions(time: 2049-07-06 07:00:00 +0000, icon: "partly-cloudy-day", windSpeed: 4.9400000000000004, temperatureMin: 59.439999999999998, temperatureMax: 86.090000000000003), Rainstorm.DarkSkyResponse.Daily.Conditions(time: 2049-07-07 07:00:00 +0000, icon: "clear-day", windSpeed: 3.8999999999999999, temperatureMin: 58.969999999999999, temperatureMax: 89.219999999999999), Rainstorm.DarkSkyResponse.Daily.Conditions(time: 2049-07-08 07:00:00 +0000, icon: "clear-day", windSpeed: 2.7599999999999998, temperatureMin: 59.939999999999998, temperatureMax: 90.879999999999995), Rainstorm.DarkSkyResponse.Daily.Conditions(time: 2049-07-09 07:00:00 +0000, icon: "clear-day", windSpeed: 2.46, temperatureMin: 62.789999999999999, temperatureMax: 92.680000000000007), Rainstorm.DarkSkyResponse.Daily.Conditions(time: 2049-07-10 07:00:00 +0000, icon: "partly-cloudy-day", windSpeed: 2.8700000000000001, temperatureMin: 62.240000000000002, temperatureMax: 92.909999999999997)]), currently: Rainstorm.DarkSkyResponse.Conditions(time: 2049-07-04 04:43:07 +0000, icon: "clear-night", summary: "Clear", windSpeed: 2.3100000000000001, temperature: 61.93))
Modify the Dark Sky API key to render it invalid. Run the application again to see the result.

Because the application is unable to fetch weather data from the Dark Sky API, it presents an alert to the user. The message informs the user about the problem and there's nothing more we can do at this point.
An error is printed to the console, indicating that the response of the Dark Sky API couldn't be decoded. That makes sense since the Dark Sky API probably returned a 401 error, indicating that the request was not authorized.
Unable to Decode JSON Response dataCorrupted(Swift.DecodingError.Context(codingPath: [], debugDescription: "The given data was not valid JSON.", underlyingError: Optional(Error Domain=NSCocoaErrorDomain Code=3840 "JSON text did not start with array or object and option to allow fragments not set." UserInfo={NSDebugDescription=JSON text did not start with array or object and option to allow fragments not set.})))
I'd like to make one more improvement in the RootViewModel class. Even if the request to the Dark Sky API doesn't fail, it may help to print the status code of the request. Before the if statement, we use optional binding to cast the response parameter to an HTTPURLResponse instance. The HTTPURLResponse class exposes the status code of the request, which may help us debug any issues that arise.
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)")
}
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 and inspect the output in the console. Because we signed the request with an invalid API key, the Dark Sky API returns a 401 response to the application.
Status Code: 401
Other Options
Defining an error type with only one case may seem excessive. I agree that we have a few other options. We could omit the error from the completion handler. Instead the RootViewController instance could simply inspect the presence or absence of weather data.
By defining an error type to inform the RootViewController instance of any problems, it becomes trivial to expand the number of scenarios the RootViewController instance can respond to. It's also a good practice to use errors to communicate if something goes wrong. Optionals are fine, but they only tell us if there's a value or not. Errors allow us to be more granular and they also have the option to provide additional information if necessary.
What's Next?
It's almost time to populate the user interface of the application, but there's one aspect of the RootViewModel class that I'd like to change before we continue. The RootViewModel class passes the weather data to the RootViewController class, which means that the RootViewController class still knows too much about the weather data and its origin. Instead of passing the DarkSkyResponse struct to the RootViewController class, it's time to add a dash of protocol-oriented programming.