赋能 UIKit 的声明式革命:@resultBuilder
在 UIStackView
中的应用 SwiftlyUI
自 iOS 13 引入的 @resultBuilder
特性为 Swift 开启了元编程新范式。其最大价值在于让传统 UIKit 视图获得声明式表达能力,尤其适合优化 UIStackView
这种基于子视图排列的容器组件。
核心实现原理:
- 创建自定义结果构造器(如
ViewBuilder
) - 扩展
UIStackView
初始化方法 - 通过闭包语法糖管理
arrangedSubviews
// DSL 驱动的新型初始化方式
let stack = UIStackView(axis: .vertical) {
UILabel("标题")
UIImageView(systemName: "star.fill")
if showButton {
SubmitButton()
}
}
DSL 内核实现:ViewBuilder 完整实现了结果构造器协议,其设计体现了三大技术取舍
@resultBuilder
public struct ViewBuilder {
public static func buildBlock(_ components: [UIView]...) -> [UIView] { components.flatMap { $0 } }
public static func buildBlock() -> [UIView] { [] }
public static func buildOptional(_ component: [UIView]?) -> [UIView] { component ?? [] }
public static func buildArray(_ components: [[UIView]]) -> [UIView] { components.flatMap { $0 } }
public static func buildEither(first: [UIView]) -> [UIView] { first }
public static func buildPartialBlock(accumulated: [UIView], next: [UIView]) -> [UIView] { accumulated + next }
...
}
视图容器实现策略:封装UIStackView
- 强制固定 axis 属性,避免运行时意外修改
- 默认采用 .center 对齐方式平衡布局灵活性
- 保留 UIStackView 原生 API 访问能力
@available(iOS 13, *)
public final class VStackView: UIStackView {
public override var axis: NSLayoutConstraint.Axis {
get { .vertical }
set { super.axis = .vertical }
}
@discardableResult
public convenience init(spacing: CGFloat = 0, @SwiftlyUIBuilder content: () -> [UIView]) {
self.init(frame: .zero)
self.axis = .vertical
self.spacing = spacing
self.alignment = .center
content().forEach { addArrangedSubview($0) }
}
}
@available(iOS 13, *)
public final class HStackView: UIStackView { ... }
@available(iOS 13, *)
public final class ZStackView: UIView {
public convenience init(@ViewBuilder content: () -> [UIView]) {
self.init(frame: .zero)
let views = content()
// 禁用自动布局标记
setLayoutActivation(false, for: views)
// 添加子视图
views.forEach { addSubview($0) }
// 重新启用布局约束
setLayoutActivation(true, for: views)
views.forEach { $0.safeActivateConstraints() }
// 应用安全边距约束
views.forEach { applyAlignmentConstraints(for: $0) }
}
private func setLayoutActivation(_ enabled: Bool, for views: [UIView]) {
views.forEach {
$0.canActiveLayout = enabled
setLayoutActivation(enabled, for: $0.subviews)
}
}
private func applyAlignmentConstraints(for view: UIView) {
let guide = layoutMarginsGuide
// 创建弹性边距约束(优先级降级避免冲突)
view.addNewConstraint(
leadingAnchor.constraint(greaterThanOrEqualTo: guide.leadingAnchor),
type: .marginsLeft
).priority = .defaultLow
// 其他方向约束逻辑类似...
}
}
链式 API 设计规范 - 通过扩展 UIView 提供流畅配置接口:
public extension UIView {
@discardableResult
func opacity(_ value: CGFloat) -> Self {
self.alpha = value
return self
}
@discardableResult
func alpha(_ value: CGFloat) -> Self {
self.alpha = value
return self
}
@discardableResult
func tag(_ tag: Int) -> Self {
self.tag = tag
return self
}
...
}
Autolayout自动布局系统封装-约束存储架构
extension UIView {
// 约束存储器
struct ConstraintHolder {
var constraints: [ConstraintType: NSLayoutConstraint] = [:]
var pending: [ConstraintType: ConstraintConfig] = [:]
}
// 约束配置模型
struct ConstraintConfig {
let type: ConstraintType
let targetType: ConstraintTargetType
let offset: CGFloat
let multiplier: CGFloat
let relation: NSLayoutConstraint.Relation
let anchor: Any? // NSLayoutXAxisAnchor | NSLayoutYAxisAnchor | NSLayoutDimension
let usesMargins: Bool
// 完整构造器...
}
func addNewConstraint(_ constraint: NSLayoutConstraint, type: ConstraintType) {
translatesAutoresizingMaskIntoConstraints = false
// 移除旧约束
removeConstraint(type: type)
var holder = constraintHolder
if superview != nil {
constraint.isActive = true // 自动激活有效约束
}
holder.constraints[type] = constraint
constraintHolder = holder
}
}
约束操作引擎
@discardableResult
func trailing(to anchor: NSLayoutXAxisAnchor, offset: CGFloat = 0) -> Self {
let config = ConstraintConfig(type: .trailing, targetType: .other, offset: offset, XAxisAnchor: anchor)
var holder = constraintHolder
holder.pendingConstraints[.trailing] = config
constraintHolder = holder
return self
}
@discardableResult
func trailing(to view: UIView, offset: CGFloat = 0) -> Self {
if view == superview {
addNewConstraint(
trailingAnchor.constraint(equalTo: view.layoutMarginsGuide.trailingAnchor, constant: -offset),
type: .trailing
)
}else {
trailing(to: view.layoutMarginsGuide.trailingAnchor, offset: offset)
}
return self
}
完整 DSL 开发范式 - 实际应用场景示例:登录表单实现
let zView = ZStackView {// == UIView
UIView()
.frame(width: 300, height: 200)
.backgroundColor(.red.opacity(0.5))
.fillSuperMargins()
VStackView(spacing: 10) {
HStackView(spacing: 10) {
Label("ACC:")
.font(.medium(14))
.width(50)
UITextField("input ACC")
.height(35)
.width(180)
}
.border(.orange)
.cornerRadius(5)
HStackView(spacing: 10) {
Label("PWD:")
.font(.medium(14))
.width(50)
UITextField("input PWD")
.height(35)
.width(180)
}
.border(.orange)
.cornerRadius(5)
}
.distribution(.fillEqually)
.centerToSuper()
}
.backgroundColor(.blue.opacity(0.5))
.padding(10)
.center(to: view)
view.addSubview(zView)