Swift SnapKit

42 阅读6分钟

1. 给谁做约束

ConstraintView

#if canImport(UIKit)
    public typealias ConstraintView = UIView
#else
    public typealias ConstraintView = NSView
#endif

对 ConstraintView 做扩展,定义一个snp属性

public extension ConstraintView {
    var snp: ConstraintViewDSL {
        return ConstraintViewDSL(view: self)
    }
    
}

ConstraintViewDSL 定义如下:

public struct ConstraintViewDSL: ConstraintAttributesDSL {
    
    @discardableResult
    public func prepareConstraints(_ closure: (_ make: ConstraintMaker) -> Void) -> [Constraint] {
        return ConstraintMaker.prepareConstraints(item: self.view, closure: closure)
    }
    
    public func makeConstraints(_ closure: (_ make: ConstraintMaker) -> Void) {
        ConstraintMaker.makeConstraints(item: self.view, closure: closure)
    }
    
    public func remakeConstraints(_ closure: (_ make: ConstraintMaker) -> Void) {
        ConstraintMaker.remakeConstraints(item: self.view, closure: closure)
    }
    
    public func updateConstraints(_ closure: (_ make: ConstraintMaker) -> Void) {
        ConstraintMaker.updateConstraints(item: self.view, closure: closure)
    }
    
    public func removeConstraints() {
        ConstraintMaker.removeConstraints(item: self.view)
    }
    
    public var contentHuggingHorizontalPriority: Float {
        get {
            return self.view.contentHuggingPriority(for: .horizontal).rawValue
        }
        nonmutating set {
            self.view.setContentHuggingPriority(LayoutPriority(rawValue: newValue), for: .horizontal)
        }
    }
    
    public var target: AnyObject? {
        return self.view
    }
    
    internal let view: ConstraintView
    
    internal init(view: ConstraintView) {
        self.view = view
        
    }
    
}

ConstraintViewDSL 提供了makeConstraintsupdateConstraints 等方法

ConstraintViewDSL 继承于 ConstraintAttributesDSL , ConstraintAttributesDSL 继承于 ConstraintBasicAttributesDSL

ConstraintBasicAttributesDSL 定义了基础属性,比如left, top, center, width , height

ConstraintAttributesDSL 提供了iOS8才有的一些属性,比如 leftMargin, firstBaseline

2. 如何设置约束

ConstraintMaker

ConstraintMaker 是设置约束的入口函数

public class ConstraintMaker {
    internal init(item: LayoutConstraintItem) {
        self.item = item
        self.item.prepare()
    }
}

prepare 函数实现, 禁用了AutoresizingMask

extension LayoutConstraintItem {
    internal func prepare() {
        if let view = self as? ConstraintView {
            view.translatesAutoresizingMaskIntoConstraints = false
        }
    }
}

ConstraintMaker 提供了属性的getter方法, 如下所示

public class ConstraintMaker {
    
    public var left: ConstraintMakerExtendable {
        return self.makeExtendableWithAttributes(.left)
    }
    
    public var top: ConstraintMakerExtendable {
        return self.makeExtendableWithAttributes(.top)
    }

    internal func makeExtendableWithAttributes(_ attributes: ConstraintAttributes) -> ConstraintMakerExtendable {
        let description = ConstraintDescription(item: self.item, attributes: attributes)
        self.descriptions.append(description)
        return ConstraintMakerExtendable(description)
    }
    // ...
}

ConstraintMaker 里包含了一个 ConstraintDescription 数组, 保存了用户设置的各个属性, 然后返回 ConstraintMakerExtendable

ConstraintAttributes

ConstraintAttributes 是一个结构体, 遵从 OptionSet, 因为swift的枚举没法将多个枚举项组成一个值,比如 ConstraintAttributes中的size,center是由多个枚举选项组合成的。 OptionSet使用了高效的位域进行表示。

    internal static let none: ConstraintAttributes = 0
    internal static let left: ConstraintAttributes = ConstraintAttributes(UInt(1) << 0)
    internal static let top: ConstraintAttributes = ConstraintAttributes(UInt(1) << 1)
    internal static let right: ConstraintAttributes = ConstraintAttributes(UInt(1) << 2)
    internal static let bottom: ConstraintAttributes = ConstraintAttributes(UInt(1) << 3)
    internal static let leading: ConstraintAttributes = ConstraintAttributes(UInt(1) << 4)
    internal static let trailing: ConstraintAttributes = ConstraintAttributes(UInt(1) << 5)
    internal static let width: ConstraintAttributes = ConstraintAttributes(UInt(1) << 6)
    internal static let height: ConstraintAttributes = ConstraintAttributes(UInt(1) << 7)
    internal static let centerX: ConstraintAttributes = ConstraintAttributes(UInt(1) << 8)
    internal static let centerY: ConstraintAttributes = ConstraintAttributes(UInt(1) << 9)
    internal static let lastBaseline: ConstraintAttributes = ConstraintAttributes(UInt(1) << 10)

    // ... 
    internal static let edges: ConstraintAttributes = [.horizontalEdges, .verticalEdges]
    internal static let horizontalEdges: ConstraintAttributes = [.left, .right]
    internal static let verticalEdges: ConstraintAttributes = [.top, .bottom]
    internal static let directionalEdges: ConstraintAttributes = [.directionalHorizontalEdges, .directionalVerticalEdges]
    internal static let directionalHorizontalEdges: ConstraintAttributes = [.leading, .trailing]
    internal static let directionalVerticalEdges: ConstraintAttributes = [.top, .bottom]
    internal static let size: ConstraintAttributes = c
    internal static let center: ConstraintAttributes = [.centerX, .centerY]

center 是由 [.centerX, .centerY], size 是由 [.width, .height]

ConstraintAttributes 重载了 + += -= == 运算符

internal func + (left: ConstraintAttributes, right: ConstraintAttributes) -> ConstraintAttributes {
    return left.union(right)
}

internal func +=(left: inout ConstraintAttributes, right: ConstraintAttributes) {
    left.formUnion(right)
}

internal func -=(left: inout ConstraintAttributes, right: ConstraintAttributes) {
    left.subtract(right)
}

internal func ==(left: ConstraintAttributes, right: ConstraintAttributes) -> Bool {
    return left.rawValue == right.rawValue
}

ConstraintMakerExtendable

ConstraintMakerExtendable 继承于ConstraintMakerRelatable, 可以实现多个属性的链式调用 比如设置left,right,top

public class ConstraintMakerExtendable: ConstraintMakerRelatable {
    
    public var left: ConstraintMakerExtendable {
        self.description.attributes += .left
        return self
    }
    
    public var top: ConstraintMakerExtendable {
        self.description.attributes += .top
        return self
    }
}

通过重载运算符 +=能够将 .left 驾到 ConstraintAttributes

ConstraintMakerRelatable

ConstraintMakerRelatable 作用是指定视图间的约束关系,比如常用的 equalTo 函数

    @discardableResult
    public func equalTo(_ other: ConstraintRelatableTarget, _ file: String = #file, _ line: UInt = #line) -> ConstraintMakerEditable {
        return self.relatedTo(other, relation: .equal, file: file, line: line)
    }


    internal func relatedTo(_ other: ConstraintRelatableTarget, relation: ConstraintRelation, 
                            file: String, line: UInt) -> ConstraintMakerEditable {
        let related: ConstraintItem
        let constant: ConstraintConstantTarget
        
        if let other = other as? ConstraintItem {
            guard other.attributes == ConstraintAttributes.none ||
                  other.attributes.layoutAttributes.count <= 1 ||
                  other.attributes.layoutAttributes == self.description.attributes.layoutAttributes ||
                  other.attributes == .edges && self.description.attributes == .margins ||
                  other.attributes == .margins && self.description.attributes == .edges ||
                  other.attributes == .directionalEdges && self.description.attributes == .directionalMargins ||
                  other.attributes == .directionalMargins && self.description.attributes == .directionalEdges else {
                fatalError("Cannot constraint to multiple non identical attributes. (\(file), \(line))");
            }
            
            related = other
            constant = 0.0
        } else if let other = other as? ConstraintView {
            related = ConstraintItem(target: other, attributes: ConstraintAttributes.none)
            constant = 0.0
        } else if let other = other as? ConstraintConstantTarget {
            related = ConstraintItem(target: nil, attributes: ConstraintAttributes.none)
            constant = other
        } else if #available(iOS 9.0, OSX 10.11, *), let other = other as? ConstraintLayoutGuide {
            related = ConstraintItem(target: other, attributes: ConstraintAttributes.none)
            constant = 0.0
        } else {
            fatalError("Invalid constraint. (\(file), \(line))")
        }
        
        let editable = ConstraintMakerEditable(self.description)
        editable.description.sourceLocation = (file, line)
        editable.description.relation = relation
        editable.description.related = related
        editable.description.constant = constant
        return editable
    }

ConstraintRelatableTarget

ConstraintRelatableTarget 是约束视图的实例,对 ConstraintRelatableTarget 进行扩展,添加更多可支持类型

public protocol ConstraintRelatableTarget {
}

......

extension ConstraintInsets: ConstraintRelatableTarget {
}

extension ConstraintItem: ConstraintRelatableTarget {
}

extension ConstraintView: ConstraintRelatableTarget {
}


public protocol ConstraintInsetTarget: ConstraintConstantTarget {
}

extension ConstraintInsetTarget {

    internal var constraintInsetTargetValue: ConstraintInsets {
        if let amount = self as? ConstraintInsets {
            return amount
        } else if let amount = self as? Float {
            return ConstraintInsets(top: CGFloat(amount), left: CGFloat(amount), bottom: CGFloat(amount), right: CGFloat(amount))
        } else if let amount = self as? Double {
            return ConstraintInsets(top: CGFloat(amount), left: CGFloat(amount), bottom: CGFloat(amount), right: CGFloat(amount))
        } else if let amount = self as? CGFloat {
            return ConstraintInsets(top: amount, left: amount, bottom: amount, right: amount)
        } else if let amount = self as? Int {
            return ConstraintInsets(top: CGFloat(amount), left: CGFloat(amount), bottom: CGFloat(amount), right: CGFloat(amount))
        } else if let amount = self as? UInt {
            return ConstraintInsets(top: CGFloat(amount), left: CGFloat(amount), bottom: CGFloat(amount), right: CGFloat(amount))
        } else {
            return ConstraintInsets(top: 0, left: 0, bottom: 0, right: 0)
        }
    }
    
}

ConstraintMakerEditable

ConstraintMakerEditable 继承于 ConstraintMakerPrioritizable ,主要提供 multipliedBy, dividedBy, inset, offset 用来设置约束的函数

public class ConstraintMakerEditable: ConstraintMakerPrioritizable {

    @discardableResult
    public func multipliedBy(_ amount: ConstraintMultiplierTarget) -> ConstraintMakerEditable {
        self.description.multiplier = amount
        return self
    }
    
    @discardableResult
    public func dividedBy(_ amount: ConstraintMultiplierTarget) -> ConstraintMakerEditable {
        return self.multipliedBy(1.0 / amount.constraintMultiplierTargetValue)
    }
    
    @discardableResult
    public func offset(_ amount: ConstraintOffsetTarget) -> ConstraintMakerEditable {
        self.description.constant = amount.constraintOffsetTargetValue
        return self
    }
    
    @discardableResult
    public func inset(_ amount: ConstraintInsetTarget) -> ConstraintMakerEditable {
        self.description.constant = amount.constraintInsetTargetValue
        return self
    }
}

ConstraintMakerPrioritizable

用来设置优先级

public class ConstraintMakerPrioritizable: ConstraintMakerFinalizable {
    
    @discardableResult
    public func priority(_ amount: ConstraintPriority) -> ConstraintMakerFinalizable {
        self.description.priority = amount.value
        return self
    }
}

ConstraintMakerFinalizable

ConstraintMakerFinalizable 包含了 ConstraintDescription

public class ConstraintMakerFinalizable {
    
    internal let description: ConstraintDescription
    
    internal init(_ description: ConstraintDescription) {
        self.description = description
    }
}

ConstraintDescription

ConstraintDescription 提供了与约束相关的所有内容

public class ConstraintDescription {
    
    internal let item: LayoutConstraintItem
    internal var attributes: ConstraintAttributes
    internal var relation: ConstraintRelation? = nil
    internal var sourceLocation: (String, UInt)? = nil
    internal var label: String? = nil
    internal var related: ConstraintItem? = nil
    internal var multiplier: ConstraintMultiplierTarget = 1.0
    internal var constant: ConstraintConstantTarget = 0.0
    internal var priority: ConstraintPriorityTarget = 1000.0
    internal lazy var constraint: Constraint? = {
        guard let relation = self.relation,
              let related = self.related,
              let sourceLocation = self.sourceLocation else {
            return nil
        }
        let from = ConstraintItem(target: self.item, attributes: self.attributes)
        
        return Constraint(
            from: from,
            to: related,
            relation: relation,
            sourceLocation: sourceLocation,
            label: self.label,
            multiplier: self.multiplier,
            constant: self.constant,
            priority: self.priority
        )
    }()
    
    // MARK: Initialization
    
    internal init(item: LayoutConstraintItem, attributes: ConstraintAttributes) {
        self.item = item
        self.attributes = attributes
    }
    
}

3. 设置完后约束后如何处理

public class ConstraintMaker {
    internal static func prepareConstraints(item: LayoutConstraintItem, closure: (_ make: ConstraintMaker) -> Void) -> [Constraint] {
        let maker = ConstraintMaker(item: item)
        closure(maker)
        var constraints: [Constraint] = []
        for description in maker.descriptions {
            guard let constraint = description.constraint else {
                continue
            }
            constraints.append(constraint)
        }
        return constraints
    }
    
    internal static func makeConstraints(item: LayoutConstraintItem, closure: (_ make: ConstraintMaker) -> Void) {
        let constraints = prepareConstraints(item: item, closure: closure)
        for constraint in constraints {
            constraint.activateIfNeeded(updatingExisting: false)
        }
    }
}

通过 closure 设置多个约束,存放在ConstraintMaker的description数组中

    internal func activateIfNeeded(updatingExisting: Bool = false) {
        guard let item = self.from.layoutConstraintItem else {
            print("WARNING: SnapKit failed to get from item from constraint. Activate will be a no-op.")
            return
        }
        let layoutConstraints = self.layoutConstraints

        if updatingExisting {
            var existingLayoutConstraints: [LayoutConstraint] = []
            for constraint in item.constraints {
                existingLayoutConstraints += constraint.layoutConstraints
            }

            for layoutConstraint in layoutConstraints {
                let existingLayoutConstraint = existingLayoutConstraints.first { $0 == layoutConstraint }
                guard let updateLayoutConstraint = existingLayoutConstraint else {
                    fatalError("Updated constraint could not find existing matching constraint to update: \(layoutConstraint)")
                }

                let updateLayoutAttribute = (updateLayoutConstraint.secondAttribute == .notAnAttribute) ? updateLayoutConstraint.firstAttribute : updateLayoutConstraint.secondAttribute
                updateLayoutConstraint.constant = self.constant.constraintConstantTargetValueFor(layoutAttribute: updateLayoutAttribute)
            }
        } else {
            NSLayoutConstraint.activate(layoutConstraints)
            item.add(constraints: [self])
        }
    }

4. SnapKit 优秀设计

1. 链式调用(Chaining)

SnapKit 使用链式调用风格,使得代码结构简洁流畅。通过链式调用,开发者可以对同一个约束对象进行连续设置,减少冗余代码。

借鉴点:

  • 使用链式调用的设计,可以让方法调用流畅、易读,避免重复的变量声明和赋值。
  • 可以在返回值中返回自身(self)或其他相关对象引用,以便在一个调用链中构建出复杂逻辑
    public var left: ConstraintMakerExtendable {
        self.description.attributes += .left
        return self
    }

链式调用配置网络请求, 链式调用适用于一些配置项的设置

class RequestBuilder {
    private var url: URL?
    private var timeoutInterval: TimeInterval = 60
    private var headers: [String: String] = [:]
    private var errors: [String] = []
    
    func setURL(_ urlString: String) -> RequestBuilder {
        if let url = URL(string: urlString) {
            self.url = url
        } else {
            errors.append("Invalid URL")
        }
        return self
    }

    func setTimeout(_ timeout: TimeInterval) -> RequestBuilder {
        if timeout > 0 {
            self.timeoutInterval = timeout
        } else {
            errors.append("Invalid timeout value")
        }
        return self
    }

    func addHeader(key: String, value: String) -> RequestBuilder {
        self.headers[key] = value
        return self
    }

    func build() -> Result<URLRequest, [String]> {
        guard errors.isEmpty else {
            return .failure(errors)
        }
        
        guard let url = url else {
            return .failure(["URL is missing"])
        }
        
        var request = URLRequest(url: url)
        request.timeoutInterval = timeoutInterval
        for (key, value) in headers {
            request.addValue(value, forHTTPHeaderField: key)
        }
        
        return .success(request)
    }
}

2. 使用 snp 属性作为命名空间

SnapKit 通过扩展 UIView 添加了 snp 属性,使得 UIView 可以直接使用 SnapKit 方法。这种命名空间的设计不仅提升了代码的简洁性,还防止了名称冲突。

借鉴点:

  • 使用命名空间封装库方法,避免与其他库或系统方法发生冲突。
  • 在命名时,选用简短且有意义的名称,使代码更加直观。
public extension ConstraintView {
    var snp: ConstraintViewDSL {
        return ConstraintViewDSL(view: self)
    }
    
}