Swift 中的 Result builders

2022/08/30 Xcode 14 iOS 16 macOS 13

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

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

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

VStack {
  Text("1")
  List {
    Text("")
  }
}

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

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

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

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

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

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

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 了:

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 即可:

static func buildBlock(_ component: UIView) -> UIView {
  component
}

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

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

然后就可以这样写:

VStack {
  if Bool.random() {
    UILabel().text("True")
  } else {
    UILabel().text("False")
  }
}

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

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

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

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

Subscribe to zzzwco
Receive the latest updates directly to your inbox.
Mint this entry as an NFT to add it to your collection.
Verification
This entry has been permanently stored onchain and signed by its creator.