One common issue with programmatic UIKit development is dealing with the creation and configuration of UI objects. Configuring UI elements in our code can be cumbersome and hard to synchronize their behavior and style across several screens.

By using the Builder Pattern, we can centralize the creation and configuration of these objects. This simplifies our codebase and decouples the creation of UI objects from our views.

What is the Builder Pattern?#

The Builder Pattern is a creational design pattern that allows us to construct complex objects step by step. It separates the construction of a complex object from its representation, allowing the same construction process to create different representations.

I don’t want to focus on the formal definition too much (you can find a lot of great articles on the topic like this one). Instead, I prefer to show you a working example, so let’s go for it.

Practical Example#

Let’s start with the builder. In this example, I want a way to configure a button with some defaults, so I can rely on these defaults to simplify further and accept modifications for specific cases.

Applying the Builder Pattern to UIKit#

Let’s see how we can use the Builder Pattern to create and configure a UIButton in UIKit.

In a Storyboard project let’s create a new file ButtonBuilder.swift and add the following code.

class ButtonBuilder {
    private var button = UIButton(type: .system)
    
    func setTamic(_ active: Bool) -> Self {
        button.translatesAutoresizingMaskIntoConstraints = active
        return self
    }

    func setTitle(_ title: String) -> Self {
        button.setTitle(title, for: .normal)
        return self
    }

    func setTitleColor(_ color: UIColor) -> Self {
        button.setTitleColor(color, for: .normal)
        return self
    }

    func setBackgroundColor(_ color: UIColor) -> Self {
        button.backgroundColor = color
        return self
    }

    func setCornerRadius(_ radius: CGFloat) -> Self {
        button.layer.cornerRadius = radius
        return self
    }

    func setAction(_ action: @escaping () -> Void) -> Self {
        button.addAction(UIAction {_ in
            action()
        }, for: .touchUpInside)
        return self
    }

    func build() -> UIButton {
        return button
    }
}

They key point to implement a builder is that we have configuration functions that return the builder itself. These functions can be chained with other configuration functions as needed. Once we are done with the configuration, we call the build function to return the actual object configured with the selected options.

Using the builder inside a ViewController#

To use our builder let’s create a new file called TestViewController.swift and add the following code.

import UIKit


class ExampleViewController: UIViewController {
    private lazy var button = ButtonBuilder()
        .setTamic(false)
        .setTitle("Example")
        .setAction {
            print("Button pressed")
        }
        .setBackgroundColor(.cyan)
        .build()
    
    override func viewDidLoad() {
        view.addSubview(button)
        NSLayoutConstraint.activate([
            button.centerXAnchor.constraint(equalTo: view.centerXAnchor),
            button.centerYAnchor.constraint(equalTo: view.centerYAnchor)
        ])
    }
}

#Preview {
    ExampleVC()
}

The final result resembles the simple and declarative style of SwiftUI!

Remember to use [weak self] if you’re capturing self or any property of self in your button action.

Test the button action in the Canvas and enjoy!

builder-final

Considerations#

Sometimes the object cannot be created before we have all the properties or dependencies. In these cases, we can gather the dependencies or properties and only create the object in the build function, where we inject the desired properties.

Here is an example for a Label to demonstrate this.

class LabelBuilder {
    private var text: String = ""
    private var numberOfLines: Int = 0
    private var tamic: Bool = false
    
    func withText(_ text: String) -> Self {
        self.text = text
        return self
    }
    
    func withNumberOfLines(_ number: Int) -> Self {
        self.numberOfLines = number
        return self
    }
    
    func withAutoLayout(_ tamic: Bool) -> Self {
        self.tamic = tamic
        return self
    }
    
    func build() -> UILabel {
        let label = UILabel()
        label.text = text
        label.numberOfLines = numberOfLines
        label.translatesAutoresizingMaskIntoConstraints = tamic
        return label
    }
}

In this example, we define the properties beforehand and assign default values that make sense for our final object. Changes to these defaults are optional and can be made using the configuration methods.

Conclusion#

By leveraging the Builder Pattern, we can streamline the process of creating and configuring UI elements in UIKit. This approach not only makes our codebase more manageable but also decouples the configuration from the views, enhancing maintainability and reusability. Whether you are setting up a simple button or a more complex object, the Builder Pattern provides a flexible and clean solution.

I hope this guide helps you simplify your UIKit development. Give it a try in your next project and see the difference it can make!