Swift 中的 Result builders

2022/08/30 Xcode 14 iOS 16 macOS 13

在以往的命令式编程中,通常这样构建视图:

1
2
3
let label = UILabel()
label.frame = self.view.bounds
self.view.addSubview(label)

但 SwiftUI 采用的是声明式语法:

1
2
3
4
5
6
VStack {
Text("1")
List {
Text("")
}
}

显而易见,SwiftUI 比 UIKit 要简洁地多,我们可以轻松地将不同的视图组合成复杂的界面。这得益于 Result builders(结果构造器),它使 SwiftUI 成为了特定领域语言(DSL)。

结果构造器是 Swift 5.4 正式引入的新特性,它可以将一系列子对象组合成新的对象,这个新对象又可以作为子对象去构造更复杂的对象。它在 SwiftUI 中无处不在,如构建场景的 @SceneBuilder,构建视图的 @ViewBuilder,还有 Swift 5.7 新增的正则构造器 RegexBuilder 等。

下面通过一个简单的示例来演示结果构造器的基本使用,假设我们想赋予 UIKit 类似 SwiftUI 的声明式语法,首先声明一个构造器:

1
2
3
4
5
6
@resultBuilder struct VStackBuilder {

static func buildBlock(_ components: UIView...) -> [UIView] {
components
}
}

如上所示,要实现一个结果构造器,需要使用 @resultBuilder 修饰,并且必须实现 buildBlock 方法。

然后可以使用 @VStackBuilder 修饰相应的构造块(build block),如下的 VStack 会从 content 闭包构建视图:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
final class VStack: UIStackView {

@discardableResult
init(superView: UIView,
spacing: CGFloat = 10.0,
@VStackBuilder content: () -> [UIView]) {
super.init(frame: .zero)
self.axis = .vertical
self.spacing = spacing
self.translatesAutoresizingMaskIntoConstraints = false
content().forEach { self.addArrangedSubview($0) }

superView.addSubview(self)
NSLayoutConstraint.activate([
self.centerXAnchor.constraint(equalTo: superView.centerXAnchor),
self.centerYAnchor.constraint(equalTo: superView.centerYAnchor),
])
}

required init(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}

然后就可以愉快地使用 VStack 了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
class ViewController: UIViewController {

override func viewDidLoad() {
super.viewDidLoad()

VStack(superView: self.view) {
UILabel()
.text("Hello")
.textColor(.orange)

UILabel()
.text("World")
.textColor(.cyan)
}
}
}

extension UILabel {

@discardableResult
func text(_ text: String?) -> Self {
self.text = text
return self
}

@discardableResult
func font(_ font: UIFont) -> Self {
self.font = font
return self
}

@discardableResult
func textColor(_ textColor: UIColor) -> Self {
self.textColor = textColor
return self
}
}

如果需要在 VStack 中条件渲染视图,只需在相应的 VStackBuilder 中实现相应的静态方法 buildEither 即可:

1
2
3
4
5
6
7
8
9
10
11
static func buildBlock(_ component: UIView) -> UIView {
component
}

static func buildEither(first component: UIView) -> UIView {
component
}

static func buildEither(second component: UIView) -> UIView {
component
}

然后就可以这样写:

1
2
3
4
5
6
7
VStack {
if Bool.random() {
UILabel().text("True")
} else {
UILabel().text("False")
}
}

关于结果构造器的更多细节和使用参考:SE-0289

结果构造器使用简单,但效果神奇!它可以快速地实现 DSL,简化工作。

上例只实现了小部分构件 UI 的功能,并不具备数据绑定和更新状态的能力。此处仅为演示结果构造器的基本用法,并无指导意义。如果你的项目支持的最低版本为 iOS 13,可桥接使用 SwiftUI。

Github 有个仓库收集了许多 Result builders 的用法,参考:awesome-result-builders

您的支持将鼓励我继续创作!
0%