It takes time to become familiar with a new programming language and some features don't always immediately make sense. Enums are a joy to work with in Swift, but it took some time before I found a use for associated values. In this episode, we refactor Rainstorm by taking advantage of enums with associated values. We remove unnecessary optionals and make the code we write more readable.
In the previous episode, I mentioned that closures with optional parameters can lead to awkward or overly complex implementations. Open RootViewModel.swift to freshen up your memory. The fetchLocation(completion:) method accepts a closure as its only argument. The closure defines two parameters, an optional Location object and an optional LocationServiceError object.
Two scenarios are possible. If the location manager successfully fetches the location of the device, the first argument of the completion handler is a Location object and the second argument is equal to nil. If the location manager fails to fetch the location of the device, the first argument of the completion handler is equal to nil and the second argument is a LocationServiceError object.
Because we're dealing with optionals, it is possible that both arguments are equal to nil. As a consumer of the API, that is an option you need to consider. We know that this isn't a possible scenario because we implemented the LocationManager class, but that's not how you should approach an API. How would you use the API if you didn't have access to the implementation of the LocationManager class?
Fetching the Location of the Device
Optionals are great and I encourage every Swift developer to embrace them. They're a core component of the language. But we don't need optionals in this scenario. There's a better solution that is elegant and that takes advantage of a neat feature of the language.
Fetching the location of the device results in one of two possible outcomes, success or failure. That makes it a perfect fit for an enum. Each scenario corresponds with a case of the enum.
Open LocationService.swift and, below the definition of LocationSerivceError, define an enum with name LocationServiceResult. The enum defines two cases, success and failure.
enum LocationServiceResult {
case success
case failure
}
Let's update the FetchLocationCompletion type alias by defining another parameter of type LocationServiceResult. That's a good start, but we're still stuck with two optional parameters.
typealias FetchLocationCompletion = (LocationServiceResult, Location?, LocationServiceError?) -> Void
Let's take another look at the two possible scenarios. If the location service successfully fetches the location of the device, we can be sure to have a Location instance and if the location service fails to fetch the location of the device, a LocationServiceError instance is created to communicate the problem downstream. Do you start to see the solution? The Location instance is only associated with the first scenario and the LocationServiceError instance is only associated with the second scenario.
Swift allows developers to store associated values of other types alongside the cases of an enum. The syntax is simple. The success case is always associated with a Location instance and the failure case is always associated with a LocationServiceError instance. In other words, the success case defines an associated value of type Location and the failure case defines an associated value of type LocationServiceError.
enum LocationServiceResult {
case success(Location)
case failure(LocationServiceError)
}
Because the Location and LocationServiceError instances are now associated with the LocationServiceResult object, we can remove the second and third parameter of FetchLocationCompletion. That means FetchLocationCompletion no longer defines optional parameters.
typealias FetchLocationCompletion = (LocationServiceResult) -> Void
This looks nice, but it's only a start. Let's take a look at the implementation of the LocationManager class. Navigate to the locationManager(_:didChangeAuthorization:) method. The didFetchLocation closure no longer accepts two arguments. It now accepts one argument of type LocationServiceResult. We define the result and pass it to the didFetchLocation closure.
func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) {
if status == .notDetermined {
...
} else if status == .authorizedWhenInUse {
...
} else {
// Location Service Result
let result: LocationServiceResult = .failure(.notAuthorizedToRequestLocation)
// Invoke Completion Handler
didFetchLocation?(result)
// Reset Completion Handler
didFetchLocation = nil
}
}
We also update the locationManager(_:didUpdateLocations:) method. We instantiate a LocationServiceResult object with a Location instance. The result is passed to the didFetchLocation closure.
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
guard let location = locations.first else {
return
}
// Location Service Result
let result: LocationServiceResult = .success(Location(location: location))
// Invoke Completion Handler
didFetchLocation?(result)
// Reset Completion Handler
didFetchLocation = nil
}
It's time for the cherry on the cake. We also need to change the implementation of the RootViewModel class. Open RootViewModel.swift and navigate to the fetchLocation() method. We replace the location and error arguments with an argument named result. Because the result argument is an enum, a switch statement is a better fit than an if-else statement.
We can extract the associated values in the switch statement. Each case gives us access to its associated value. We move the code from the if-else statement to the switch statement. We can remove the code of the second else clause. We no longer need it.
private func fetchLocation() {
locationService.fetchLocation { [weak self] (result) in
switch result {
case .success(let location):
// Fetch Weather Data
self?.fetchWeatherData(for: location)
case .failure(let error):
print("Unable to Fetch Location (\(error))")
// Invoke Completion Handler
self?.didFetchWeatherData?(nil, .notAuthorizedToRequestLocation)
}
}
}
I'm sure you agree that the updated implementation of fetchLocation() looks much nicer and, thanks to the names of the cases of the LocationServiceResult enum, it also becomes more readable. It's clear what happens if fetching the location of the device is successful and what happens if something goes wrong.
Fetching Weather Data
We can apply the same technique to fetching weather data. Open RootViewModel.swift. Before we start, I'd like to fix a minor issue by renaming the DidFetchWeatherDataCompletion type alias to FetchWeatherDataCompletion. That name is in line with the FetchLocationCompletion type alias. Right-click the type alias and choose Rename... from the Refactor menu.

Update the name of the type alias and click Rename.

import Foundation
class RootViewModel: NSObject {
...
// MARK: - Type Aliases
typealias FetchWeatherDataCompletion = (WeatherData?, WeatherDataError?) -> Void
// MARK: - Properties
var didFetchWeatherData: FetchWeatherDataCompletion?
...
}
We define an enum with name WeatherDataResult. The enum defines two cases, success and failure. The success case has an associated value of type WeatherData. The failure case has an associated value of type WeatherDataError.
import Foundation
class RootViewModel: NSObject {
// MARK: - Types
enum WeatherDataResult {
case success(WeatherData)
case failure(WeatherDataError)
}
...
}
We replace the parameters of the FetchWeatherDataCompletion type alias with a single parameter of type WeatherDataResult.
typealias FetchWeatherDataCompletion = (WeatherDataResult) -> Void
We need to update the arguments that are passed to the didFetchWeatherData closure in five locations. We start with the fetchLocation() method. If the location manager fails to fetch the location of the device, the root view model invokes the didFetchWeatherData closure.
We create a WeatherDataResult instance and associate it with a WeatherDataError object, notAuthorizedToRequestLocation. The WeatherDataResult instance is passed as the only argument to the didFetchWeatherData closure.
private func fetchLocation() {
locationService.fetchLocation { [weak self] (result) in
switch result {
case .success(let location):
// Fetch Weather Data
self?.fetchWeatherData(for: location)
case .failure(let error):
print("Unable to Fetch Location (\(error))")
// Weather Data Result
let result: WeatherDataResult = .failure(.notAuthorizedToRequestLocation)
// Invoke Completion Handler
self?.didFetchWeatherData?(result)
}
}
}
We repeat these steps in the fetchWeatherData(for:) method. If something goes wrong, we pass noWeatherDataAvailable as the error. If the root view model successfully creates a DarkSkyResponse instance, the instance is associated with the WeatherDataResult object and passed to the didFetchWeatherData closure.
private func fetchWeatherData(for location: Location) {
// Initialize Weather Request
let weatherRequest = WeatherRequest(baseUrl: WeatherService.authenticatedBaseUrl, location: 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)")
}
DispatchQueue.main.async {
if let error = error {
print("Unable to Fetch Weather Data \(error)")
// Weather Data Result
let result: WeatherDataResult = .failure(.noWeatherDataAvailable)
// Invoke Completion Handler
self?.didFetchWeatherData?(result)
} else if let data = data {
// Initialize JSON Decoder
let decoder = JSONDecoder()
// Configure JSON Decoder
decoder.dateDecodingStrategy = .secondsSince1970
do {
// Decode JSON Response
let darkSkyResponse = try decoder.decode(DarkSkyResponse.self, from: data)
// Weather Data Result
let result: WeatherDataResult = .success(darkSkyResponse)
// Invoke Completion Handler
self?.didFetchWeatherData?(result)
} catch {
print("Unable to Decode JSON Response \(error)")
// Weather Data Result
let result: WeatherDataResult = .failure(.noWeatherDataAvailable)
// Invoke Completion Handler
self?.didFetchWeatherData?(result)
}
} else {
// Weather Data Result
let result: WeatherDataResult = .failure(.noWeatherDataAvailable)
// Invoke Completion Handler
self?.didFetchWeatherData?(result)
}
}
}.resume()
}
Before we can run the application, we need to update the RootViewController class. Open RootViewController.swift and navigate to the setupViewModel(with:) method. Replace the arguments of the didFetchWeatherData closure with a single argument named result.
The if-else statement is substituted for a switch statement with two cases, success and failure. We move the code from the if-else statement to the switch statement.
private func setupViewModel(with viewModel: RootViewModel) {
// Configure View Model
viewModel.didFetchWeatherData = { [weak self] (result) in
switch result {
case .success(let weatherData):
// Initialize Day View Model
let dayViewModel = DayViewModel(weatherData: weatherData.current)
// Update Day View Controller
self?.dayViewController.viewModel = dayViewModel
// Initialize Week View Model
let weekViewModel = WeekViewModel(weatherData: weatherData.forecast)
// Update Week View Controller
self?.weekViewController.viewModel = weekViewModel
case .failure(let error):
let alertType: AlertType
switch error {
case .notAuthorizedToRequestLocation:
alertType = .notAuthorizedToRequestLocation
case .failedToRequestLocation:
alertType = .failedToRequestLocation
case .noWeatherDataAvailable:
alertType = .noWeatherDataAvailable
}
// Notify User
self?.presentAlert(of: alertType)
}
}
}
That's it. Build and run the application to make sure we didn't break anything. The application hasn't changed visually, but the project is in much better shape thanks to the use of enums with associated values.
What's Next?
I'm a big fan of optionals and the safety they add to the code I write. But optionals aren't always the right choice. This episode shows that the Swift language has other features that are sometimes more suited to solve a problem.
Enums with associated values may feel a bit foreign at first, but I can assure you that the more you use them the more uses you find for them.