为 UIKit 注入声明式基因——基于 @resultBuilder 的 DSL 构建实践

赋能 UIKit 的声明式革命:@resultBuilderUIStackView 中的应用 SwiftlyUI

自 iOS 13 引入的 @resultBuilder 特性为 Swift 开启了元编程新范式。其最大价值在于让传统 UIKit 视图获得声明式表达能力,尤其适合优化 UIStackView 这种基于子视图排列的容器组件。

核心实现原理

  1. 创建自定义结果构造器(如 ViewBuilder
  2. 扩展 UIStackView 初始化方法
  3. 通过闭包语法糖管理 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)
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容