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 capturingself
or any property ofself
in your button action.
Test the button action in the Canvas and enjoy!
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!