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.
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) {
}