iOS SnapKit源码解析(一)makeConstraints的过程

写在前面

经过激烈的思想斗争,笔者从Android开发转到了iOS开发,发现了两者很多的共同之处,这里就不在赘述;不过最大的不适应体现在UI方面,Android的布局编写和预览更舒适。

万般无奈之下,接触到了SnapKit,一个用Swift编写的AutoLayout框架,极大程度上简化了纯布局的代码。

分析源码

本文只探究makeConstraints的过程,也就是停留在闭包之外。

ConstraintView

SnapKit的最基本用法:

view.snp.makeConstraints { (make) in

}

首先view.snp很容易让人想到是使用了扩展,但并不是直接对UIView的扩展,而是要引入一个新的概念ConstraintView,具体情况在ConstraintView.swift中体现:

#if os(iOS) || os(tvOS)
    import UIKit
#else
    import AppKit
#endif

#if os(iOS) || os(tvOS)
    public typealias ConstraintView = UIView
#else
    public typealias ConstraintView = NSView
#endif

这就是该文件所有的代码了,可以看到,通过判断当前系统做了两件事:

  1. 包的导入:如果当前系统是iOS或者tvOS,那么导入UIKit,否则导入AppKit
  2. 类的重命名:如果当前系统是iOS或者tvOS,那么将UIView重命名为ConstraintView,否则将NSView重命名为ConstraintView。其中typealias用于为已存在的类重新命名,提高代码的可读性。

总而言之,ConstraintView是为了适配多平台而定义的UIViewNSView的别称。

extension ConstraintView

紧接上文,view.snp是对ConstraintView的扩展,在ConstraintView+Extensions.swift中返回:

#if os(iOS) || os(tvOS)
    import UIKit
#else
    import AppKit
#endif

public extension ConstraintView {
    // 此处略去很多废弃的方法
    
    public var snp: ConstraintViewDSL {
        return ConstraintViewDSL(view: self)
    }
    
}

此处省略了该文件中很多被废弃的方法,只看最关键的变量snp,此处返回了一个新的对象ConstraintViewDSL,并以自己,一个ConstraintView作为参数。

注意:在SnapKit中,几乎所有文件开头都有关于导入UIKit还是AppKit的判断,之后就不再展示这段重复的代码。

ConstraintViewDSL

接下来jump到ConstraintViewDSL.swift文件中,这里只展示它的一个最关键方法:

public struct ConstraintViewDSL: ConstraintAttributesDSL {

    public func makeConstraints(_ closure: (_ make: ConstraintMaker) -> Void) {
        ConstraintMaker.makeConstraints(item: self.view, closure: closure)
    }
    internal let view: ConstraintView
    
    internal init(view: ConstraintView) {
        self.view = view
    }
}

首先可以看到ConstraintViewDSL是一个结构体,实现了ConstraintAttributesDSL接口,构造函数也非常简单,只接收一个ConstraintView并保存起来;另外,view.snp.makeConstraints也只是把保存的ConstraintView,连同传递进来的闭包一起交给ConstraintMaker处理。

除了makeConstraints方法,还有remakeConstraintsupdateConstraintsremoveConstraints等方法,因为都是交给ConstraintMaker处理,所以不再赘述。

ConstraintMaker

ConstraintMaker.swift文件中:

public class ConstraintMaker {

    private let item: LayoutConstraintItem
    private var descriptions = [ConstraintDescription]()
    
    internal init(item: LayoutConstraintItem) {
        self.item = item
        self.item.prepare()
    }

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

}

ConstraintMaker是一个,从上面展示的代码可以知道创建约束的基本流程:首先makeConstraints调用prepareConstraints,构造一个maker,然后由闭包调用这个maker,遍历makerdescriptions,将获取的约束添加到一个约束数组constraints中,然后prepareConstraints执行完毕并将约束返回这个constraintsmakeConstraints继续执行,获取这些约束,然后激活。

构造maker时,传入构造函数的item应为保存在ConstraintViewDSL中的ConstraintView,但在init声明中变成了LayoutConstraintItem

LayoutConstraintItem

LayoutConstraintItem.swift

public protocol LayoutConstraintItem: class {
}

extension ConstraintView : LayoutConstraintItem {
}

可以看到这是一个协议,并且ConstraintView实现了它,协议中也实现了一些方法,其中就包括prepare

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

prepare方法禁用了从AutoresizingMaskConstraints的自动转换,即translatesAutoresizingMaskIntoConstraints可以把 frame ,bouds,center 方式布局的视图自动转化为约束形式,转化的结果就是自动添加需要的约束;而此时我们需要自己添加约束,必然会产生冲突,所以直接指定这个视图不去使用约束布局。

中途休息一下

到目前为止,我们知道了调用view.snp.makeConstraints时,这个view经过一系列转运,最终禁用了自己的约束布局,而这个过程仅仅是prepareConstraints方法的第一行,也就是只调用了ConstraintMaker的构造函数,接下来继续分析prepareConstraints

    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
    }

构造maker之后,先是执行了闭包的内容(不在本文讨论范围内),紧接着创建了一个包含Constraint的数组constraints;然后遍历包含了ConstraintDescription类型的descriptions数组(该数组是maker成员变量,具体可以往上翻翻),并试图将每个description中包含的constraint添加到constraints数组中,最后返回该数组。

ConstraintDescription

ConstraintDescription.swift

public class ConstraintDescription {
    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
        )
    }()
}

此处略去了很多成员变量,简单来说,ConstraintDescription内部持有一个Constraint变量,需要时可以利用自己的成员变量构造出一个Constraint并返回。

Constraint

Constraint.swift中,关键代码在构造函数,略去成员变量和方法,以及构造函数中关于多平台的适配之后,内容精简如下:

internal init(...) {
    self.layoutConstraints = []
    // get attributes
    let layoutFromAttributes = self.from.attributes.layoutAttributes
    let layoutToAttributes = self.to.attributes.layoutAttributes
    
    // get layout from
    let layoutFrom = self.from.layoutConstraintItem!
    
    // get relation
    let layoutRelation = self.relation.layoutRelation
    
    for layoutFromAttribute in layoutFromAttributes {
        // get layout to attribute
        let layoutToAttribute: NSLayoutAttribute
        if layoutToAttributes.count > 0 {
            if self.from.attributes == .edges && self.to.attributes == .margins {
                switch layoutFromAttribute {
                case .left:
                    layoutToAttribute = .leftMargin
                case .right:
                    layoutToAttribute = .rightMargin
                case .top:
                    layoutToAttribute = .topMargin
                case .bottom:
                    layoutToAttribute = .bottomMargin
                default:
                    fatalError()
                }
            } else if self.from.attributes == .margins && self.to.attributes == .edges {
                switch layoutFromAttribute {
                case .leftMargin:
                    layoutToAttribute = .left
                case .rightMargin:
                    layoutToAttribute = .right
                case .topMargin:
                    layoutToAttribute = .top
                case .bottomMargin:
                    layoutToAttribute = .bottom
                default:
                    fatalError()
                }
            } else if self.from.attributes == self.to.attributes {
                layoutToAttribute = layoutFromAttribute
            } else {
                layoutToAttribute = layoutToAttributes[0]
            }
        } else {
            if self.to.target == nil && (layoutFromAttribute == .centerX || layoutFromAttribute == .centerY) {
                layoutToAttribute = layoutFromAttribute == .centerX ? .left : .top
            } else {
                layoutToAttribute = layoutFromAttribute
            }
        }
        // get layout constant
        let layoutConstant: CGFloat = self.constant.constraintConstantTargetValueFor(layoutAttribute: layoutToAttribute)
        
        // get layout to
        var layoutTo: AnyObject? = self.to.target
        
        // use superview if possible
        if layoutTo == nil && layoutToAttribute != .width && layoutToAttribute != .height {
            layoutTo = layoutFrom.superview
        }
        
        // create layout constraint
        let layoutConstraint = LayoutConstraint(
            item: layoutFrom,
            attribute: layoutFromAttribute,
            relatedBy: layoutRelation,
            toItem: layoutTo,
            attribute: layoutToAttribute,
            multiplier: self.multiplier.constraintMultiplierTargetValue,
            constant: layoutConstant
        )
        
        // set label
        layoutConstraint.label = self.label
        
        // set priority
        layoutConstraint.priority = self.priority.constraintPriorityTargetValue
        
        // set constraint
        layoutConstraint.constraint = self
        
        // append
        self.layoutConstraints.append(layoutConstraint)
    }
}

首先创建layoutConstraints来保存最后生成的所有LayoutConstraint(继承自NSLayoutConstraint),然后获取该约束的起始对象的约束属性layoutFromAttributes和目标对象的约束属性layoutToAttributes。接下来的主要逻辑就在循环体内,通过遍历起始对象的约束属性,然后获取目标对象的约束属性,最终创建一条新的约束。

至此,我们可以认为prepareConstraints执行完毕,makeConstraints已经获取到了所有需要的约束,接下来要执行最后一步:激活约束

activateIfNeeded

这是Constraint.swift中的一个方法:

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

这里首先获取了起始目标item,类型为LayoutConstraintItem,有变量constraintsSet来保存所有的约束;

然后获取了自己的layoutConstraints数组,Constraint不单指一个约束,而是layoutConstraints中所有约束的集合,或者说是snp.makeConstraints过程中的一个集合;

最后通过NSLayoutConstraint.activate激活了整个layoutConstraints数组中的约束,并且将这些约束添加到了起始目标的约束集合中保存起来。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,772评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,458评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,610评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,640评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,657评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,590评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,962评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,631评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,870评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,611评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,704评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,386评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,969评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,944评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,179评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,742评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,440评论 2 342