I love working with enums in Swift. While that may sound odd, I feel enums are undervalued and developers miss out on some of their nicer applications, especially when combined with associated values.

The networking layer we build in Building a Modern Networking Layer in Swift relies heavily on enums and associated values. In this episode, I would like to show you another neat application of enums and how they can help you create flexible, elegant APIs that are a pleasure to use.

Using Enums to Create Builders

In From Zero to App Store, I show you how to use the builder pattern to generate a Cloudinary URL from a source URL. Cloudinary is a service that manipulates and transforms media, including images. In From Zero to App Store, we use Cloudinary to size and convert images fetched from a remote server. Let me walk you through the implementation of the builder.

import UIKit

final class CloudinaryURLBuilder {

    // MARK: - Properties

    private let source: URL

    // MARK: -

    private var width: Int?
    private var height: Int?

    // MARK: - Initialization

    init(source: URL) {
        // Set URL
        self.source = source
    }

    func width(_ width: Int) -> CloudinaryURLBuilder {
        // Update Width
        self.width = width

        return self
    }

    func height(_ height: Int) -> CloudinaryURLBuilder {
        // Update Height
        self.height = height

        return self
    }

    func build() -> URL {
        var parameters: [String] = []
        var url = URL(string: "https://res.cloudinary.com/cocoacasts/image/fetch/")!

        if let width = width {
            parameters.append("w_\(width)")
        }

        if let height = height {
            parameters.append("h_\(height)")
        }

        // Define Format
        parameters.append("f_png")

        // Define Device Pixel Ratio
        let dpr = String(format: "%1.1f", UIScreen.main.scale)
        parameters.append("dpr_\(dpr)")

        // Append Parameters
        if !parameters.isEmpty {
            let parametersAsString = parameters.joined(separator: ",")
            url = url.appendingPathComponent(parametersAsString)
        }

        return url.appendingPathComponent(source.absoluteString)
    }

}

We initialize a builder instance with a source URL. It is the source URL we send to Cloudinary. Cloudinary fetches the image the source URL points to and applies the transformations you define. The CloudinaryURLBuilder class exposes a number of methods to define which transformations need to be applied to the image the source URL points to. The width(_:) and height(_:) methods can be used to set the size of the image Cloudinary returns. This is useful to increase your application's performance. It shouldn't display images that are too large for the device your application runs on. A service like Cloudinary makes that a breeze.

Because the width(_:) and height(_:) methods return the CloudinaryURLBuilder instance, we can take advantage of method chaining. Let me show you how this works. We create a CloudinaryURLBuilder instance, passing the source URL to the initializer. We invoke zero or more methods on the builder to configure it. To generate the Cloudinary URL, we invoke build() on the CloudinaryURLBuilder instance. The build() method returns the Cloudinary URL, which we can use in the application. Cloudinary handles the transformations.

let source = URL(string: "https://cdn.cocoacasts.com/building-a-sign-in-form-using-swiftui/building-a-sign-in-form-using-swiftui.svg")!

CloudinaryURLBuilder(source: source)
    .width(200)
    .build()

Enums and Associated Values

I like the builder pattern. The resulting API is clear and the builder pattern promotes immutability. I would like to show you how to use enums and associated values to create another API for the CloudinaryURLBuilder class.

We start by defining a nested enum with name Modifier. Each case of the Modifier enum defines a transformation. We define three cases, width, height, and format. Because each transformation needs some type of input, each case has an associated value. The width and height cases have an associated value of type Int. The format case has an associated value of type String.

import UIKit

final class CloudinaryURLBuilder {

    // MARK: - Types

    enum Modifier {
        case width(Int)
        case height(Int)
        case format(String)
    }
    
    ...

}

With the possible transformations defined by the Modifier enum, we can put them to use. We define a build(_:) method that accepts an array of Modifier objects. Like the other build() method, it returns a URL object.

func build(_ modifiers: [Modifier]) -> URL {
	
}

We use the implementation of the other build() method as a starting point. As the name suggests, the parameters variable stores the parameters we intend to send to Cloudinary. Each parameter corresponds with a transformation. We apply the map(_:) method to the array of modifiers, assigning the result to parameters.

func build(_ modifiers: [Modifier]) -> URL {
    var url = URL(string: "https://res.cloudinary.com/cocoacasts/image/fetch/")!

    var parameters = modifiers.map { modifier in
        
    }

    ...
}

In the closure we pass to the map(_:) method, we switch on the modifier object. We return a string for each case, using the associated value to construct the parameter.

func build(_ modifiers: [Modifier]) -> URL {
    var url = URL(string: "https://res.cloudinary.com/cocoacasts/image/fetch/")!

    var parameters = modifiers.map { modifier in
        switch modifier {
        case .width(let width):
            return "w_\(width)"
        case .height(let height):
            return "h_\(height)"
        case .format(let format):
            return "f_\(format)"
        }
    }

    ...
}

The remainder of the implementation is similar to that of the other build() method. Let's see how the APIs differ in use. We create a CloudinaryURLBuilder instance and invoke the build(_:) method. To define the transformations, we pass an array of Modifier objects to the build(_:) method. That's it.

CloudinaryURLBuilder(source: source)
    .width(200)
    .build()

CloudinaryURLBuilder(source: source).build(
    [.width(200), .format("jpg")]
)

Variadic Parameters in Swift

We can improve the build(_:) method by using a variadic parameter. Revisit the build(_:) method and change the type of the modifiers parameter from an array of Modifier objects to a variadic parameter. The three dots following the Modifier type indicate the modifiers parameter is a variadic parameter. That's the only change we need to make to the build(_:) method.

func build(_ modifiers: Modifier...) -> URL {
	...
}

The call site also changes. We no longer need to pass an array of Modifier objects to the build(_:) method. We can remove the square brackets of the array. Even though the change is subtle, it is a nice illustration of how variadic parameters can improve an API.

CloudinaryURLBuilder(source: source).build(
    .width(200),
    .format("jpg")
)

Type Safety

There are a few more changes I would like to make. The associated values of the width and height cases are of type Int. Let's change these to UInt to make sure negative values are flagged by the compiler.

enum Modifier {
    case width(UInt)
    case height(UInt)
    case format(String)
}

The compiler throws an error if you try to pass in a negative width or height.

Writing Elegant Code with Enums

The associated value of the format case is of type String. That isn't a good idea. Let's define a nested enum, Format, with raw values of type String. We define two cases, png and jpg. We change the type of the associated value of the format case to Format. This simple change avoids typos and stringly typed code.

// MARK: - Types

enum Modifier {
    case width(UInt)
    case height(UInt)
    case format(Format)
}

enum Format: String {
    case png
    case jpg
}

In the build(_:) method, we use the raw value of the Format object to construct the parameter.

func build(_ modifiers: Modifier...) -> URL {
    var url = URL(string: "https://res.cloudinary.com/cocoacasts/image/fetch/")!

    var parameters = modifiers.map { modifier in
        switch modifier {
        case .width(let width):
            return "w_\(width)"
        case .height(let height):
            return "h_\(height)"
        case .format(let format):
            return "f_\(format.rawValue)"
        }
    }

    ...

}

With these changes, we eliminated the risk for common errors, such as negative values and typos.

CloudinaryURLBuilder(source: source).build(
    .width(200),
    .format(.jpg)
)

What's Next?

Enums have some limitations compared to structs and classes, but they are surprisingly versatile. The pattern we used in this episode illustrates how we can leverage enums and associated values to build an API that is clean, easy to use, and trivial to extend. Even though the original implementation of the CloudinaryURLBuilder class worked well, extending the builder is much easier using enums and associated values. To add support for a new transformation, we extend the Modifier enum and update the build(_:) method. That's it.