Swift NSLayoutConstraint 布局扩展类似于 Masonry 的简洁 API

540 阅读2分钟

最近入职新公司,发现Swift项目并没有引入任何的布局第三方框架,导致还在用最原始的NSLayoutConstraint布局

addSubview(titleLabel)
addSubview(iconView)

NSLayoutConstraint.activate([
    iconView.topAnchor.constraint(equalTo: topAnchor, constant: Constant.iconTopMargin),
    iconView.leftAnchor.constraint(equalTo: leftAnchor, constant: Constant.iconLeftMargin),
    iconView.rightAnchor.constraint(equalTo: rightAnchor, constant: -Constant.iconLeftMargin),

    titleLabel.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -Constant.titleBottomMargin),
    titleLabel.leftAnchor.constraint(equalTo: leftAnchor, constant: Constant.titleLeftMargin),
    titleLabel.rightAnchor.constraint(equalTo: rightAnchor, constant: -Constant.titleLeftMargin),
])

说实话看得我头皮发麻,真的感觉生存在原始社会的感觉。但由于公司引入第三方框架很麻烦,要多方评审,才能引入,于是有了自己封装一个简单的NSLayoutConstraint 布局扩展类似于 Masonry 的简洁 API 的扩展。

以下封装使得我们可以使用更简洁的语法来设置约束。我们将 Anchor 相关的使用替换为更直观的属性访问,同时为 UIView 添加 bottomcenterX 等属性。这种方法使得使用者可以更自然地设置约束。以下是扩展实现:

NSLayoutConstraint 扩展

import UIKit

// MARK: - UIView Extension

extension UIView {
    
    @discardableResult
    func makeConstraints(_ constraints: (ConstraintMaker) -> Void) -> [NSLayoutConstraint] {
        let maker = ConstraintMaker(view: self)
        constraints(maker)
        let activatedConstraints = maker.constraints
        NSLayoutConstraint.activate(activatedConstraints)
        return activatedConstraints
    }
    
    // MARK: - Properties for Constraints

    var top: NSLayoutYAxisAnchor {
        return self.topAnchor
    }

    var bottom: NSLayoutYAxisAnchor {
        return self.bottomAnchor
    }

    var leading: NSLayoutXAxisAnchor {
        return self.leadingAnchor
    }

    var trailing: NSLayoutXAxisAnchor {
        return self.trailingAnchor
    }

    var centerX: NSLayoutXAxisAnchor {
        return self.centerXAnchor
    }

    var centerY: NSLayoutYAxisAnchor {
        return self.centerYAnchor
    }
}

// MARK: - ConstraintMaker Class

class ConstraintMaker {
    private var constraints: [NSLayoutConstraint] = []
    private weak var view: UIView?

    init(view: UIView) {
        self.view = view
    }
    
    // MARK: - Top Constraint

    @discardableResult
    func top(_ anchor: NSLayoutYAxisAnchor) -> Offset {
        return Offset { constant in
            guard let view = self.view else { return }
            let constraint = view.top.constraint(equalTo: anchor, constant: constant)
            self.constraints.append(constraint)
        }
    }

    // MARK: - Bottom Constraint
    
    @discardableResult
    func bottom(_ anchor: NSLayoutYAxisAnchor) -> Offset {
        return Offset { constant in
            guard let view = self.view else { return }
            let constraint = view.bottom.constraint(equalTo: anchor, constant: constant)
            self.constraints.append(constraint)
        }
    }

    // MARK: - Leading Constraint
    
    @discardableResult
    func leading(_ anchor: NSLayoutXAxisAnchor) -> Offset {
        return Offset { constant in
            guard let view = self.view else { return }
            let constraint = view.leading.constraint(equalTo: anchor, constant: constant)
            self.constraints.append(constraint)
        }
    }
    
    // MARK: - Trailing Constraint
    
    @discardableResult
    func trailing(_ anchor: NSLayoutXAxisAnchor) -> Offset {
        return Offset { constant in
            guard let view = self.view else { return }
            let constraint = view.trailing.constraint(equalTo: anchor, constant: constant)
            self.constraints.append(constraint)
        }
    }

    // MARK: - Center Constraints

    @discardableResult
    func centerX(_ anchor: NSLayoutXAxisAnchor) -> Self {
        guard let view = self.view else { return self }
        let constraint = view.centerX.constraint(equalTo: anchor)
        constraints.append(constraint)
        return self
    }

    @discardableResult
    func centerY(_ anchor: NSLayoutYAxisAnchor) -> Self {
        guard let view = self.view else { return self }
        let constraint = view.centerY.constraint(equalTo: anchor)
        constraints.append(constraint)
        return self
    }

    // MARK: - Size Constraints

    @discardableResult
    func width(_ constant: CGFloat) -> Self {
        guard let view = self.view else { return self }
        let constraint = view.width.constraint(equalToConstant: constant)
        constraints.append(constraint)
        return self
    }

    @discardableResult
    func height(_ constant: CGFloat) -> Self {
        guard let view = self.view else { return self }
        let constraint = view.height.constraint(equalToConstant: constant)
        constraints.append(constraint)
        return self
    }

    // MARK: - Aspect Ratio

    @discardableResult
    func aspectRatio(_ ratio: CGFloat) -> Self {
        guard let view = self.view else { return self }
        let constraint = view.width.constraint(equalTo: view.height, multiplier: ratio)
        constraints.append(constraint)
        return self
    }
}

// MARK: - Offset Class

class Offset {
    private let closure: (CGFloat) -> Void

    init(closure: @escaping (CGFloat) -> Void) {
        self.closure = closure
    }

    func offset(_ value: CGFloat) {
        closure(value)
    }
}

使用示例

现在你可以使用新的简洁语法,例如 myView.bottomview.centerX,而不需要使用 Anchor。示例如下:

import UIKit

class MyViewController: UIViewController {
    let myView = UIView()
    let anotherView = UIView()

    override func viewDidLoad() {
        super.viewDidLoad()
        
        myView.backgroundColor = .blue
        anotherView.backgroundColor = .red
        
        myView.translatesAutoresizingMaskIntoConstraints = false
        anotherView.translatesAutoresizingMaskIntoConstraints = false
        
        view.addSubview(myView)
        view.addSubview(anotherView)
        
        // 设置 myView 的约束
        myView.makeConstraints { make in
            make.top(view.safeAreaLayoutGuide.top).offset(20) // 使用 offset
            make.leading(view.leading).offset(20)
            make.trailing(view.trailing).offset(-20)
            make.height(200)
        }
        
        // 设置 anotherView 的约束
        anotherView.makeConstraints { make in
            make.top(myView.bottom).offset(20)
            make.centerX(view.centerX) // 不需要 Anchor
            make.width(150)
            make.height(100)
        }
    }
}

小结

通过这种方式,约束的设置变得更加直观和简洁,符合现代 Swift 编程风格。你可以自由地访问 UIViewtopbottomleadingtrailingcenterXcenterY 属性,而不必显式使用 Anchor 后缀。这种封装非常灵活,适合用于各种复杂的布局场景。