Swift and Cocoa Essentials

Taking Advantage of Swift's Native Result Type

Swift and Cocoa Essentials
1 Threads, Queues, and Concurrency 12:02
2 What Is the Main Thread Plus 09:29
3 Increasing Performance Through Caching Plus 16:13
4 What Is Asynchronous Programming Plus 03:58
5 Protecting the Secrets of Your Mobile Application Plus 13:46
6 Taking Advantage of Swift's Native Result Type 07:39

Most of the Cocoa APIs we use to build applications are driven by Objective-C. This doesn't mean we need to use Objective-C to take advantage of these APIs, but it does mean that the APIs lack some of the niceties you expect from a Swift API.

Take the URLSession API as an example. Sending a request to a remote API is straightforward using the URLSession API. The implementation is straightforward, but it doesn't feel swifty. Let me show you what I mean.

Using the URLSession API

Fire up Xcode and create a playground by choosing the Blank template from the iOS > Playground section. Name the playground Networking.

Setting Up a Playground in Xcode 11

Remove the contents of the playground and add an import statement for the UIKit framework.

import UIKit

I would like to fetch an image from a remote server using the URLSession API. We create the URL and, since we are working in a playground, we use the exclamation mark to forced unwrap the result of the initialization.

import UIKit

// Create URL
let url = URL(string: "https://goo.gl/wV9G4I")!

To fetch the data for the image, we create a URLSessionDataTask instance. We ask the shared URL session for a data task, passing in the URL of the remote resource and a completion handler, a closure. The completion handler is executed when the data task completes, successfully or unsuccessfully.

import UIKit

// Create URL
let url = URL(string: "https://goo.gl/wV9G4I")!

// Create Data Task
let dataTask = URLSession.shared.dataTask(with: url) { (data, _, error) in

}

The completion handler accepts three arguments, an optional Data object, an optional URLResponse object, and an optional Error object. For this example, we are interested in the Data object and the Error object. To create a UIImage instance, we safely unwrap the Data object and use it to create a UIImage instance.

import UIKit

// Create URL
let url = URL(string: "https://goo.gl/wV9G4I")!

// Create Data Task
let dataTask = URLSession.shared.dataTask(with: url) { (data, _, error) in
    if let data = data, let image = UIImage(data: data) {
        print(image)
    } else {

    }
}

This doesn't look too bad, but we still need to handle the error in the completion handler. That is where the implementation becomes awkward. The error that is passed to the completion handler is of an optional type. In theory, it is possible that the Data object and the Error object are both equal to nil. If we want to play it safe, we need to safely unwrap the Error object in the else clause of the completion handler.

import UIKit

// Create URL
let url = URL(string: "https://goo.gl/wV9G4I")!

// Create Data Task
let dataTask = URLSession.shared.dataTask(with: url) { (data, _, error) in
    if let data = data, let image = UIImage(data: data) {
        print(image)
    } else {
        if let error = error {
            print(error)
        } else {
            print("no data, no error")
        }
    }
}

If the Data object and the Error object would both be equal to nil, we would have to throw an unknown error to inform the application that the image could not be fetched from the remote server for an unknown reason.

According to the documentation, this should never happen. The documentation states that the Error object is equal to nil if the request is successful. If the request is unsuccessful, the Data object is equal to nil and the Error object is not equal to nil.

With the documentation in mind, we can update the implementation of the completion handler by using an else if clause to safely unwrap the Error object. This may look fine, but we still have a problem. We introduced a code smell. If an if statement includes one or more else if clauses, the last else if clause should be followed by an else clause. This is similar to the default clause in a switch statement. You want to program defensively by making sure you catch any scenarios you didn't catch in the if or else if clauses.

import UIKit

// Create URL
let url = URL(string: "https://goo.gl/wV9G4I")!

// Create Data Task
let dataTask = URLSession.shared.dataTask(with: url) { (data, _, error) in
    if let data = data, let image = UIImage(data: data) {
        print(image)
    } else if let error = error {
        print(error)
    }
}

And that takes us back to square one.

import UIKit

// Create URL
let url = URL(string: "https://goo.gl/wV9G4I")!

// Create Data Task
let dataTask = URLSession.shared.dataTask(with: url) { (data, _, error) in
    if let data = data, let image = UIImage(data: data) {
        print(image)
    } else if let error = error {
        print(error)
    } else {
        print("unknown error")
    }
}

Introducing the Result Type

Soon after the introduction of the Swift programming language, developers came up with a solution for this pesky problem. I also introduced a solution in Building a Weather Application From Scratch. Let's take a look at the implementation of the RootViewModel class. In the fetchWeatherData(for:) method, the root view model uses a URLSessionDataTask instance to fetch weather data from the Dark Sky API. In the completion handler, we safely unwrap the Error object and the Data object. If both are equal to nil the else clause is executed.

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)

                    // Update User Defaults
                    UserDefaults.didFetchWeatherData = Date()

                    // 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()
}

What is interesting about the implementation is the use of the WeatherDataResult type. Let me show you what it looks like. The WeatherDataResult enum defines two cases, success and failure. The success case defines an associated value of type WeatherData. The failure case defines an associated value of type WeatherDataError.

import UIKit

class RootViewModel: NSObject {

    // MARK: - Types

    enum WeatherDataResult {
        case success(WeatherData)
        case failure(WeatherDataError)
    }

    enum WeatherDataError: Error {
        case notAuthorizedToRequestLocation
        case failedToRequestLocation
        case noWeatherDataAvailable
    }

    ...

}

This is the solution we have been looking for and it is a solution that isn't available in Objective-C. The WeatherDataResult enum clearly communicates the outcome of the weather data request, it is successful or unsuccessful. If it is successful, then it has an associated value of type WeatherData. If it is unsuccessful, then it has an associated value of type WeatherDataError. There is no third scenario.

Introducing Swift's Native Result Type

I created Building a Weather Application From Scratch before the introduction of Swift 5. That is why the RootViewModel class defines the WeatherDataResult type. Swift 5 has made everything much easier and consistent by introducing the Result type. Let's refactor the RootViewModel class by replacing the WeatherDataResult type with Swift's native Result type.

Let's start by taking a look at Swift's Result type. It defines two cases, success and failure. It also defines two generic types, Success and Failure with Failure required to inherit the Error protocol. This is a very clever solution. The success case of the Result enum has an associated value of type Success and the failure case of the Result enum has an associated value of type Failure.

/// A value that represents either a success or a failure, including an
/// associated value in each case.
public enum Result<Success, Failure> where Failure : Error {

    /// A success, storing a `Success` value.
    case success(Success)

    /// A failure, storing a `Failure` value.
    case failure(Failure)

    ...
}

With this in mind, we can put the Result type to use in the RootViewModel class. The solution is simpler than you might think thanks to the WeatherDataResult enum. We define a type alias with name WeatherDataResult. The type of the type alias is Result<WeatherData, WeatherDataError>.

import UIKit

class RootViewModel: NSObject {

    // MARK: - Type Aliases

    typealias WeatherDataResult = Result<WeatherData, WeatherDataError>

    // MARK: - Types

    enum WeatherDataResult {
        case success(WeatherData)
        case failure(WeatherDataError)
    }

    enum WeatherDataError: Error {
        case notAuthorizedToRequestLocation
        case failedToRequestLocation
        case noWeatherDataAvailable
    }

    ...
}

The only other change we need to make is remove the WeatherDataResult enum.

import UIKit

class RootViewModel: NSObject {

    // MARK: - Type Aliases

    typealias WeatherDataResult = Result<WeatherData, WeatherDataError>

    // MARK: - Types

    enum WeatherDataError: Error {
        case notAuthorizedToRequestLocation
        case failedToRequestLocation
        case noWeatherDataAvailable
    }

    ...
}

That's it. We have successfully replaced the WeatherDataResult enum with Swift's Result type. By using a type alias, we don't have to replace every occurrence of WeatherDataResult with Result<WeatherData, WeatherDataError>. Using a type alias is optional, but it improves the readability.

What's Next?

The addition of Swift's native Result enum is most welcome because it eliminates the need to define a custom Result enum. What makes this solution powerful is the combination of enums, generics, and associated values. This combination isn't available in Objective-C and that is why some Objective-C APIs don't feel as elegant as native Swift APIs.