The builder pattern isn't a common pattern in Swift and Cocoa development and you don't find it in any of Apple's frameworks. It is one of the Gang of Four design patterns and widely used in Java development.

As the name suggests, the builder pattern is aimed at object creation and configuration. The idea is simple. You pass the requirements of the object you want to create to a builder. The builder uses those requirements to create and configure the object. You could see the builder as a factory that is capable of creating a range of variations or representations of one or more types.

Building URLs

In this episode, I show you how to use the builder pattern to create URLs. Services such as Cloudinary make it easy to fetch and manipulate remote images. Cloudinary's fetch API is easy to use and makes it trivial to create the image you need. Let's explore Cloudinary's fetch API before we implement the builder pattern.

We start with a Cloudinary base URL that is specific to your account.

https://res.cloudinary.com/cocoacasts/image/fetch/

You append the URL of the remote image you want to download. When you make the request, Cloudinary downloads the image and returns it. It also caches the image. The next time that image is accessed through Cloudinary, the cached image is returned. In other words, Cloudinary also acts as a content delivery network or CDN.

https://res.cloudinary.com/cocoacasts/image/fetch/https://cocoacasts.com/exmple.svg

But Cloudinary is more than a CDN. You can pass additional parameters to Cloudinary to manipulate and transform the image. In this example, the SVG image should be converted to a PNG image. This is as simple as passing a parameter to the Cloudinary fetch API.

https://res.cloudinary.com/cocoacasts/image/fetch/f_png/https://cocoacasts.com/exmple.svg

You can also pass Cloudinary's fetch API the pixel density of the device. A device with a retina display has a pixel density of 2 or 3. The resolution of the image returned by Cloudinary is modified for the pixel density of the device.

https://res.cloudinary.com/cocoacasts/image/fetch/f_png,dpr_2.0/https://cocoacasts.com/exmple.svg

We can also constrain the dimensions of the returned image. Scrolling a table or collection view needs to be snappy and that is only possible if the displayed images are sized appropriately. Cloudinary makes that trivial. In this example, we ask Cloudinary to return an image that is 200.0 points wide. On a device with a pixel density of 2 the returned image is 400.0 pixels wide.

https://res.cloudinary.com/cocoacasts/image/fetch/w_200,f_png,dpr_2.0/https://cocoacasts.com/exmple.svg

How to Implement the Builder Pattern

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

Swift Patterns: Builders

Remove the contents of the playground with the exception of the import statement for UIKit. We define the URL of the remote image we want to fetch using Cloudinary's fetch API.

import UIKit

let imageUrl = URL(string: "https://cdn.cocoacasts.com/assets/cocoacasts.svg")!

The next step is defining the Cloudinary base URL.

import UIKit

let imageUrl = URL(string: "https://cdn.cocoacasts.com/assets/cocoacasts.svg")!

let url = URL(string: "https://res.cloudinary.com/cocoacasts/image/fetch/")!

The remote image we want to fetch is an SVG (Scalable Vector Graphics) image. Even though asset catalogs support the SVG format, the UIKit framework doesn't have built-in support for SVG images.

We can use Cloudinary to work around this limitation. You can pass a parameter to the fetch API to convert an image from one format to another. Let's convert the SVG image to a PNG image by appending the f_png parameter to the base URL.

import UIKit

let imageUrl = URL(string: "https://cdn.cocoacasts.com/assets/cocoacasts.svg")!

let url = URL(string: "https://res.cloudinary.com/cocoacasts/image/fetch/")!
    .appendingPathComponent("f_png")

We convert the URL of the remote image to a string and append it to the Cloudinary URL.

import UIKit

let imageUrl = URL(string: "https://cdn.cocoacasts.com/assets/cocoacasts.svg")!

let url = URL(string: "https://res.cloudinary.com/cocoacasts/image/fetch/")!
    .appendingPathComponent("f_png")
    .appendingPathComponent(imageUrl.absoluteString)

We can also define the size of the image by appending width and height parameters. In this example, we set the width to 200.0 points.

import UIKit

let imageUrl = URL(string: "https://cdn.cocoacasts.com/assets/cocoacasts.svg")!

let url = URL(string: "https://res.cloudinary.com/cocoacasts/image/fetch/")!
    .appendingPathComponent("f_png")
    .appendingPathComponent(imageUrl.absoluteString)
    .appendingPathComponent("w_\(200)")

We can set the device pixel ratio through the dpr parameter.

import UIKit

let imageUrl = URL(string: "https://cdn.cocoacasts.com/assets/cocoacasts.svg")!

let url = URL(string: "https://res.cloudinary.com/cocoacasts/image/fetch/")!
    .appendingPathComponent("f_png")
    .appendingPathComponent(imageUrl.absoluteString)
    .appendingPathComponent("w_\(200)")
    .appendingPathComponent("dpr_2")

Creating a Builder

Cloudinary's API is easy to use, but you don't want to create the URLs for remote images like this. This solution doesn't scale and it isn't easy to unit test. It also makes moving to a different provider a pain.

Let's use the builder pattern to encapsulate the creation of the URLs for remote images and remove the need for string literals at the call site. The primary goal of the solution we are about to implement is adding flexibility and reusability.

We need to create an object that helps construct or build the Cloudinary URL. We first define the builder, a final class with name ImageURLBuilder. The class defines a private, constant property, source, of type URL. The value of source is the URL of the remote image we send to Cloudinary.

final class ImageURLBuilder {

    // MARK: - Properties

    private let source: URL

}

Before we implement the API, we define an initializer that accepts one argument, source, of type URL. We set the source property in the body of the initializer.

final class ImageURLBuilder {

    // MARK: - Properties

    private let source: URL

    // MARK: - Initialization

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

}

The ImageURLBuilder class builds the Cloudinary URL. We start with a basic API. I would like to add the ability to specify the width and/or height of the returned image. We define a private, variable property, width, of type Int? and a private, variable property, height, of type Int?. The ImageURLBuilder class should not expose any state. The URL is configured through the public API of the ImageURLBuilder class. That is why width and height are declared privately.

final class ImageURLBuilder {

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

}

Let's implement the public API. We define a method with name width(_:) that accepts an integer as its only argument. Notice that it returns an ImageURLBuilder instance. Why that is becomes clear in a moment. The implementation of the width(_:) method is simple. The width property is set and self is returned from the method.

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

    return self
}

We repeat these steps for the height property. We define a method with name height(_:) that accepts an integer as its only argument. The height property is set and self is returned from the method.

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

    return self
}

Once the ImageURLBuilder instance is configured, the build() method is invoked to create or build the object, the Cloudinary URL in this example. We could define build() as a computed property. By defining it as a method, it is clear that the ImageURLBuilder instance performs an action. We start by declaring a few helper variables, parameters, an empty array of String objects, and url, the Cloudinary base URL.

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

We safely unwrap the width property. If width isn't equal to nil, we append it as a parameter to the parameters array. The format of the parameter is w_\(width). We repeat this step for the height property. If height isn't equal to nil, we append it as a parameter to the parameters array. The format of the parameter is h_\(height).

func build() -> URL {
    // Helpers
    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)")
    }
}

We also append the format parameter to the array of parameters.

func build() -> URL {
    // Helpers
    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")
}

The last parameter we add is the device pixel density parameter. We ask the main screen for the value of its scale property, convert it to a string, and append it to the array of parameters.

func build() -> URL {
    // Helpers
    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)")
}

We concatenate the array of strings if parameters isn't empty, using a comma to separate the strings. The resulting string is appended to the Cloudinary base URL.

func build() -> URL {
    // Helpers
    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)
    }
}

The last step is appending the source URL as a string to the Cloudinary URL and returning it from the build() method.

func build() -> URL {
    // Helpers
    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)
}

Let's use the ImageURLBuilder class to create the URL we created at the start of this episode. We first instantiate an instance of the ImageURLBuilder class, passing in the source URL of the remote image. We invoke the width(_:) method on the builder to define the width of the returned image. To build the Cloudinary URL, we invoke the build() method. The results panel on the right shows the resulting URL.

ImageURLBuilder(source: imageUrl)
    .width(200)
    .build()

We can use method chaining because the width(_:) and height(_:) methods return self, the ImageURLBuilder instance. That is why those methods return self.

The API is clean, elegant, and easy to work with. We could take it a step further and eliminate the number literal we pass to the width(_:) method. This is as simple as defining a nested enum that maps to an integer. This subtle change increases the readability of the API and we no longer need to use number literals at the call site.

final class ImageURLBuilder {

    // MARK: - Types

    enum Size: Int {

        case small = 100
        case medium = 200
        case large = 400
    }

    // MARK: - Properties

    private let source: URL

    // MARK: -

    private var width: Size?
    private var height: Size?

    // MARK: - Initialization

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

    // MARK: - Public API

    func width(_ width: Size) -> ImageURLBuilder {
        // Update Width
        self.width = width

        return self
    }

    func height(_ height: Size) -> ImageURLBuilder {
        // Update Height
        self.height = height

        return self
    }

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

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

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

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

}
ImageURLBuilder(source: imageUrl)
    .width(.medium)
    .build()

Injecting Dependencies

To decouple the ImageURLBuilder class from the UIKit framework and improve its testability, I recommend injecting the scale factor of the screen. We define a property for the scale factor, scale, of type CGFloat.

private let scale: CGFloat

The initializer accepts the scale factor of the screen as an argument. To keep the API clean and concise, we default to the scale factor of the main screen. In a unit test, we would pass the scale factor to the initializer to be in control of the unit test and its outcome.

// MARK: - Initialization

init(source: URL, scale: CGFloat) {
    // Set Properties
    self.source = source
    self.scale = scale
}

In the build() method, we no longer need to reference UIScreen.

func build() -> URL {
    ...

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

    ...
}

Pros and Cons

I first came across the builder pattern in one of Google's SDKs several years ago. I have to admit that I wasn't immediately convinced of the benefits of the pattern. There are several ways to adopt the builder pattern and if you don't get it right it feels verbose and isn't pleasant to use.

The most important benefit of the builder pattern is that the creation and configuration of the object are encapsulated by the builder. This is useful to control how the object is used and configured in the project. Because the builder is responsible for the creation and configuration of the object, you can keep tight control over both aspects.

Encapsulation has another nice benefit. The ImageURLBuilder class doesn't expose the Cloudinary fetch API. If we ever want to switch to a different provider, we only need to modify the ImageURLBuilder class. That is a major benefit in my book.

Another benefit is that builders get rid of lengthy initializers and they promote immutability. The builder encapsulates the information it needs to create the object and exposes an elegant and flexible API.

The builder pattern also makes unit testing much easier. You define the input for the builder and verify that the output, the object the builder creates, is configured the way you expect it to be.

The most important downside is that you need two objects to create one object. In other words, if you use the builder pattern extensively in a project, you can end up with many types and files. That is a price I am willing to pay for these benefits.

What's Next?

Even though I use the builder pattern sparingly in the projects I work on, I very much appreciate the benefits builders bring to a codebase. Use the pattern when it feels appropriate. Not every object should be created by a builder.