UIKit builder pattern

SwiftUI introduces a way to write UI code declaratively. Can we use the same paradigm with UIKit? We will show you how.

In this tutorial, we will explain how to create a user interface using the builder pattern, and the second part will show how to wrap a container element in Swift builder. In the end of this tutorial, you will be able to build UIs like this one:

Below is sample code for your reference.

let vStack = UIVStack {
    UIImageView(image: UIImage(named:"banner"))
    UIView()
        .backgroundColor(.clear)
        .heightAnchor(height: 20)
    UILabel()
        .text(title)
        .font(UIFont.systemFont(ofSize: UIFont.largeSize))
        .textAlignment(.center)
        .color(.black60)
    UILabel()
        .text(subtitle)
        .font(UIFont.systemFont(ofSize: UIFont.normalSize))
        .textAlignment(.center)
        .color(.black60)
        .numberOfLines(0)

    UIButton()
        .spStyle()
        .title("Update Now")
        .tap(action: { [weak self] in
            self?.navigateToAppStore()
        })
        .heightAnchor(height: 44)
}

How can we build a UI using Builder pattern

To write a simple Login form in the UIKit, we usually do:

let txtUserName = UITextField()
txtUserName.placeHolder = "User Name"
txtUserName.textColor = .black8
//For text change event.
txtUserName.delegate = self

let txtPassword = UITextField()
txtPassword.placeHolder = "Password"
txtPassword.textColor = .black8
//For hide password
txtPassword.style = .password
txtPassword.delegate = self

For a small project, it is acceptable to use your own design. However, with a large project, it is important to use standard design techniques so that the code can be reused and so that the application will be easy to maintain.

Usually, the original data type will be overridden and new components created following the style that the designer gives us. For example:

class MyStyleBlackTextFiled: UITextField {
	func setupUI() {
        self.textColor = .black8
        self.font = UIFont(systemFontOfSize: 18)
        self.backgroundColor = .white
    }
}

let txtUserName = MyStyleBlackTextFiled()
let password = MyStyleBlackTextFiled()

Everything is fine until one day the designer presents us with a new page, which contains different text, background color and font size.

We can create a new MyStyleRedTextField with the above implementation, but cannot reuse it as flexibly as we would like. How can we fix this?

One way is to use configuration settings like:

let textField = UITextField()

textField.config(textColor: .red, font: .system, backgroundColor: .white)

extension UITextField {
	func config(textColor: UIColor, font: UIFont, backgroundColor: UIColor) {
        //set
    }
}

However, what happens if we need to customize other properties of UITextField or add a new custom function? How can we sync with the design and reuse code?

Introduce to @discardableResult

Swift language offers @discardableResult, a feature that allows you to use or ignore the return value of a function without compiler or editor complaints.

For example, the following function returns a String:

func hello() -> String {
	"Hello"
}

Declare hello() then—The editor will warn you that hello() is not being used.

To silence it we can use the underscore character: _ = hello() or let _ = hello()

With @discardableResult

@discardableResult()
func hello() -> String {
	"Hello"
}

Use:

Declare hello(), and no warning happens

And you can assign a value to a variable with let helloString = hello().

Introduce to Extension

The iOS-MacOS developer is familiar with the concept of Extensions. With an Extension, we can add more functionality to existing Objects. For example:

extension UILabel {
    func textColor(_ color: UIColor) {
        self.textColor = color
    }
    func backgroundColor(_ color: UIColor) {
        self.backgroundColor = color
    }
}

use:
let label = UILabel()
label.textColor = .red
label .backgroundColor = .blue

Mixing @discardableResult with Extension is Builder.

extension UILabel {
    @discardableResult
    func text(_ string: String) -> UILabel {
        self.text = text
        return self
    }
    @discardableResult
    func textColor(_ color: UICOlor) -> UILabel {
        self.textColor = color
        return self
    }
}

Through the implementation of the above ideas, we can achieve:

let label = UILabel()
    .textColor(.red)
    .text("Hello")

Because label is a UILabel, you can still use any of its built-in functions and methods. For example, you can access information about it and set new properties.

let text = label.text
let background = text.backgroundColor

label.text = "ABC"

Create your own style by making an extension using the same technique.

extension UILabel {
    @discardableResult
    func myRedStyle() -> UILabel {
        self.textColor(.red).backgroundColor(.green)
        return self
    }
}

let redLabel = UILabel().text("I'm red").myRedStyle()

Using @discardableResult with Extension gives us all of the benefits of reusability, flexibility, maintainability, and the ability to expand our code while retaining the original data type.

Mentioned in
sticker #1
Subscribe to Dwarves Memo

Receive the latest updates directly to your inbox.