Swift 5 4 resultbuilder

The uikit-builder-pattern enables us to create and configure a UIView object. This article is part two of the series that explains how we can wrap a container element in Swift by using the builder pattern.

How can we wrap the container elements using Swift builder?

Function Builder

Function Builders are used in SwiftUI to create VStacks. If you've heard about the VStack component, it was built using Function Builders.

VStack {
    Text("Hello world")
    Text("Dwarves Foundation Better Engineering")
}

@resultBuilder

Swift 5.4 introduces @resultBuilder, a new feature that makes it even easier to use SwiftUI. This new feature also extends Swift's DSL capabilities to standard Swift language, allowing you to take advantage of DSLs in more areas of your codebase (the detail can be found here).

From now on you can easily write HTML forms in Swift as follows:

HTML {
    Body {
        P { "Hello HTML" }
        DIV {
            P{}
            P{}
            DIV{}
         }
    }
}

Let's extend UIKit to write an app in a DSL style. In the previous tutorial, we used Builder Pattern to create UILabel("ABC", red). Let's add a container View to make it even better. UIKit has Stack Views, which help us arrange subviews horizontally or vertically.

Let's make it from:

let label = UILabel().text("ABC").backgroundColor(.red)

let stackView = UIStackView()
stackView.distribution = .center
stackView.axis = .horizontal
stackView.addArrangeSubview(label)

To:

UIHStack {
    UILabel().text("ABC").backgroundColor(.red)
}

Turn UIKit into DSL styling

Let's define a DSL UIViewBuilder. A DSL UIViewBuilder turns a list of UIViews into a UIView.

@resultBuilder
public enum UIViewBuilder {
    public static func buildBlock(_ components: UIView...) -> [UIView] {
       components
    }
}

Use:

let views = UIViewBuilder.buildBlock(UILabel(), UIImageView(), UIView())
@UIViewBuilder func createUI() -> [UIView] {
    UILabel()
    UIImageView()
}

Note: You may ask "Why is [UIView] being the return type instead of UIView?" We will discuss that later.

With above UIViewBuilder we get a array of views from DSL syntax. It helps us to write the code naturally without constant addSubview code writing.

It is possible to write in DSL style, but the above code is not pretty. Let's write some popular UI wrapper for convenience usage.

public class UIVStack: UIStackView {
    public convenience init(@UIViewBuilder _ builder: () -> [UIView]) {
        self.init(arrangedSubviews: builder())
        self.distribution = .fill
        self.spacing = 16
        self.axis = .vertical
    }
}

Now we have convenience UIVStack:

UIStackView {
    UILabel()
    UIImageView()
    UITextField()
}

As you can see, replacing UIView with the array of UIViews allows us to add them directly to the StackView instead of calling addSubview multiple times.

Let's add some more components.

public class UIHStack: UIStackView {
    public convenience init(@UIViewBuilder _ builder: () -> [UIView]) {
        self.init(arrangedSubviews: builder())
        self.distribution = .fill
        self.spacing = 16
        self.axis = .horizontal
    }
}

public class UIZStack: UIView {
    convenience init(@UIViewBuilder _ builder: () -> [UIView]) {
        self.init()
        builder().forEach { view in
            self.addSubview(view)
            view.translatesAutoresizingMaskIntoConstraints()
            view.fitToSuperView()
        }
    }
}

By combining the technique from the last article with @resultBuilder, we can write UI using UIKit that closely matches Swift.

ios_uikit_builder_pattern_bannerpng

UIZStack(spacing: 16) {
    UIVStack(spacing: 16) {
        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()
            .mintStyle()
            .title("Update Now")
            .tap(action: { [weak self] in
                navigateToAppStore()
            })
            .heightAnchor(height: 44)
    }
}

Bonus parts

Support if-else and loop

The @resultBuilder module has a static function named buildEither that can be used to add if-else statements to your DSL as well as a function named builderArray that can be used to loop through data. These two functions work just like buildBlock does:

public static func buildEither(first component: [UIView]) -> [UIView] {
}
public static func buildEither(second component: [UIView]) -> [UIView] {
}
public static func buildArray(_ components: [[UIView]]) -> [UIView] {
}
UIHStack {
    if true {
        UIView().backgroundColor(.red)
    } else {
        UIView().backgroundColor(.green)
    }

    for image in images {
        UIImageView(image: image)
    }
}

Config container

Adding configuration to the init method to set the container view.

public class UIVStack: UIStackView {
    public convenience init(alignment: UIViewAlignment = .center, spacing: CGFloat = 16,  @UIViewBuilder _ builder: () -> [UIView]) {
        self.init(arrangedSubviews: builder())
        self.distribution = .fill
        self.spacing = spacing
        self.alignment = alignment
        self.axis = .vertical
    }
}

UIHStack(alignment: .trailling, spacing: 8) {

}
Mentioned in
sticker #1
Subscribe to Dwarves Memo

Receive the latest updates directly to your inbox.