iOS SnapKit源码解析(二)进入闭包

在上一篇 iOS SnapKit源码解析(一)makeConstraints的过程
中,简单介绍了snp.makeConstraints的调用栈,没有描述闭包之内代码的运行过程,这篇文章就探究一下闭包之内发生了什么。

简单用法

    button.snp.makeConstraints { (make) in
        make.size.equalTo(CGSize(width: 100, height: 100))
        make.center.equalTo(bgView)
    }

    label.snp.makeConstraints { (make) in
        make.top.equalTo(button.snp.bottom).offset(30) 
        make.size.equalTo(CGSize(width: 200, height: 50))
        make.centerX.equalTo(button)
    }

可以看到,make是闭包的关键所在,接下来继续研究上一篇探索过的ConstraintMaker

ConstraintMaker

这可以说是整个SnapKit中最关键的类,它将ConstraintViewConstraint联系到了一起,是将约束附加到控件上的工具。上篇文章中说道ConstraintMakerprepareConstraints方法中闭包的执行就避开不谈了,本文将从这里入手:

    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
    }

closure(maker)执行了闭包中的代码,例如:

make.size.equalTo(CGSize(width: 100, height: 100))

这里的make.size到底是怎么实现的呢?

不难发现makeConstraintMaker)有很多成员变量:

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

    // ......
}

可以看到这些ConstraintMakerExtendable变量都是甩锅侠,都交给了ConstraintMakermakeExtendableWithAttributes方法处理:

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

首先创建了一个ConstraintDescription,然后将其作为参数,创建ConstraintMakerExtendable并返回。

ConstraintMakerExtendable

public class ConstraintMakerExtendable: ConstraintMakerRelatable {
    // ......
    public var size: ConstraintMakerExtendable {
        self.description.attributes += .size
        return self
    }
}

这个类并没有构造函数,而是千篇一律的类似sizelayout属性,可以自行脑补,将size换成其他诸如widthcenterX等等众多属性,简单来说这个类的作用还是甩锅。

虽然没有找到构造函数,但是return self说明make.size已经将这个ConstraintMakerExtendable返回了,紧接着调用equalTo方法。因为这个类即没有这个方法,也没有我们想要的构造函数,于是去父类看看。

ConstraintMakerRelatable

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

    @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 editable = ConstraintMakerEditable(self.description)
        editable.description.sourceLocation = (file, line)
        editable.description.relation = relation
        editable.description.related = related
        editable.description.constant = constant
        return editable
    }
}

到这里算是会师了,不仅找到了接受ConstraintDescription为参数的构造函数,而且equalTo也在这里。这里equalTo@discardableResult值得注意,说明有两种情况,一种是像我们上面的例子一样,最多只执行到equalTo,忽略返回值;另一种就是可以在返回值上进行更进阶的操作。

internal enum ConstraintRelation : Int {
    case equal = 1
    case lessThanOrEqual
    case greaterThanOrEqual
    
    internal var layoutRelation: LayoutRelation {
        get {
            switch(self) {
            case .equal:
                return .equal
            case .lessThanOrEqual:
                return .lessThanOrEqual
            case .greaterThanOrEqual:
                return .greaterThanOrEqual
            }
        }
    }
}

不管是equalTo,还是lessThanOrEqualTo或者greaterThanOrEqual,最终都是调用relatedTo,只不过传入的枚举类ConstraintRelation不同而已。

    @discardableResult
    public func equalToSuperview(_ file: String = #file, _ line: UInt = #line) -> ConstraintMakerEditable {
        guard let other = self.description.item.superview else {
            fatalError("Expected superview but found nil when attempting make constraint `equalToSuperview`.")
        }
        return self.relatedTo(other, relation: .equal, file: file, line: line)
    }

而像equalToSuperview这种涉及到Superview的关系,都是在方法内部先获取到Superview,然后将其作为约束的目标对象,再通过对应的equalTo返回结果。

    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 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
    }

relatedTo主要是对传入other: ConstraintRelatableTarget进行转型判断,根据不同结果对related: ConstraintItemconstant: ConstraintConstantTarget进行赋值,最终构造并返回一个ConstraintMakerEditable对象。

ConstraintMakerEditable

public class ConstraintMakerEditable: ConstraintMakerPriortizable {

    @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
    }
    
}

在这里又见到了@discardableResult,如果按照最初的例子make.size.equalTo(),到这里已经执行完毕了。这些方法只不过是更深入的操作,例如offset设置偏移量,设置约束优先级等等。

ConstraintDescription

精简代码如下:

public class ConstraintDescription {
    internal var attributes: ConstraintAttributes
    // 省略很多成员变量  
    internal lazy var constraint: Constraint? = {   
        return Constraint(
            // 利用那些成员变量构造Constraint
        )
    }()
    
    // MARK: Initialization
    
    internal init(item: LayoutConstraintItem, attributes: ConstraintAttributes) {
        self.item = item
        self.attributes = attributes
    }
    
}

ConstraintDescription有很多成员变量,其中包括我们需要的attributesconstraint,需要返回constraint时,就利用这些成员变量构造一个新的Constraint并返回。

推荐阅读更多精彩内容