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.
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.