SnapKit源码分析

SnapKit是基于NSLayoutConstraint封装的一个轻量级的布局框架.区别于iOS9.0中苹果引入的系统框架NSLayoutAnchor。其实NSLayoutAnchor是一个工厂类,类似NSNumber这样的设计思想.

开始

当我们开始写约束的时候,一般都从
view.snp.makeConstraints()方法开始,通过点击snp我们进到里面看,发现它是下面这个样子:

// ConstraintView 实际上就是UIView
extension ConstraintView {
    public var snp: ConstraintViewDSL {
        return ConstraintViewDSL(view: self)
    }
}

我们现在知道了实现约束的功能其实跟这个ConstraintViewDSL类有很大关系,这里也是我们理解snapKit库如何实现开始的地方,下面开始详细介绍这个类。

ConstraintViewDSL

查看源码我们发现这个类遵守了一个ConstraintAttributesDSL协议。这个协议里面没有定义属性和方法,默认实现了一些功能:

protocol ConstraintAttributesDSL: ConstraintBasicAttributesDSL { }
extension ConstraintAttributesDSL {
    public var top: ConstraintItem {
        return ConstraintItem(target: self.target, attributes: ConstraintAttributes.top)
    }
    public var bottom: ConstraintItem {
        return ConstraintItem(target: self.target, attributes: ConstraintAttributes.bottom)
    }
    ...
}

现在我们还不知道ConstraintItem是做什么的,不过没关系,下面会详细说到。接着回到ConstraintViewDSL类里面,可以看到有一些我们常用到的方法:

public func makeConstraints(_ closure: (_ make: ConstraintMaker) -> Void) {
    ConstraintMaker.makeConstraints(item: self.view, closure: closure)
}
public func updateConstraints(_ closure: (_ make: ConstraintMaker) -> Void) {
    ConstraintMaker.updateConstraints(item: self.view, closure: closure)
}

讲到这里我们可以了解到,项目中常用到的view.snp.makeConstraints() 方法和 view.snp.bottom 其实都是在ConstraintViewDSL类里面定义的。那现在关于ConstraintViewDSL类我们就先讲到这吧,下面我们重点去了解ConstraintMaker和ConstraintItem是干什么的。

ConstraintMaker

写一段最简单的代码,让ConstraintMaker和我们见见面。

view.snp.makeConstraints({make: ConstraintMaker in 
    make.top.equalTo(20)
})

这是我们有疑问,为什么作者不直接在ConstraintMaker上面写约束呢,给我们ConstraintViewDSL类有什么用呢,下面我们将这种框架核心简单来实现下,站在作者的角度去看待问题:

  1. 首先我们自己来定义一个ConstraintMaker类,内部实现如下:
class ConstraintMaker {
    
    var item: UIView
    var descriptions = [Constraint]()
    
    init(item: UIView) {
        self.item = item
    }
    
    static func prepareConstraints(item: UIView, closure: (_ make: ConstraintMaker) -> Void) -> [Constraint] {
        let maker = ConstraintMaker(item: item)
        closure(maker)
        return maker.descriptions
    }
    
    static func makeConstraints(item: UIView, closure: (_ make: ConstraintMaker) -> Void) {
        let constraints = prepareConstraints(item: item, closure: closure)
        for constraint in constraints {
            constraint.active()
        }
    }
}

extension ConstraintMaker {
    
    var bottom: ConstraintMakerExtendable {
        // 这里先简单实现下ConstraintAttributes
        let attr = ConstraintAttributes()
        return makeExtendableWithAttributes(attr)
    }
    
    var top: ConstraintMakerExtendable {
        // 这里先简单实现下ConstraintAttributes
        let attr = ConstraintAttributes()
        return makeExtendableWithAttributes(attr)
    }
    
    private func makeExtendableWithAttributes(_ attributes: ConstraintAttributes) -> ConstraintMakerExtendable {
        // 这里存在着将ConstraintAttributes对象转化成了Constraint对象
        let constraint = Constraint()
        descriptions.append(constraint)
        return ConstraintMakerExtendable()
    }
}

这些就是snapKit实现约束的核心方法了,为了不报错,这里把剩下的几个辅助的类也定义了,它们内部只实现了一些简单的方法:

class Constraint {
    func active() { print("开始布局啦") }
}

class ConstraintMakerExtendable {
    func calc() { print("计算约束") }
}

class ConstraintAttributes {
    static var top = ConstraintAttributes()
    static var bottom = ConstraintAttributes()
}

接着我们还需要定义一个ConstraintViewDSL类,用来承载具体的约束:

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

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

大功告成,现在我们也可以自己写一个约束库了,在外面调用:

let view1 = UIView()
view1.snp.makeConstraints { (make) in
    let bottom: ConstraintMakerExtendable = make.bottom
    bottom.calc()
    
    let top: ConstraintMakerExtendable = make.top
    top.calc()
}

打印如下:

计算约束
计算约束
开始布局啦
开始布局啦

回到ConstraintMaker中继续讲

SnapKit的作者写了很多的类,当我们第一眼看到这么多类,会觉得无从下手,不知道从哪开始阅读,下面我大概整理了一下,约束的过程。

// 一条完整的约束一般是这样的:
make.top.left.equalTo().offset().priority()

//链式调用的关系如下:
make: ConstraintMaker
    .top: ConstraintMakerExtendable
        .left: ConstraintMakerExtendable(继承自下面的ConstraintMakerRelatable)
             .equalTo: ConstraintMakerRelatable
                      .offset: ConstraintMakerEditable(继承自下面的ConstraintMakerPriortizable)
                              .priority: ConstraintMakerPriortizable
// .offset、.priority类继承关系
ConstraintMakerEditable -> ConstraintMakerPriortizable -> ConstraintMakerFinalizable
// .top、.left、.equalTo类继承关系
ConstraintMakerExtendable -> ConstraintMakerRelatable

通过阅读源码可以看到,调用view.snp.makeConstraints()方法,实际上内部是先调用prepareConstraints方法将约束准备好,在调用activate()将约束添加到视图上:

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

activeate方法作为添加约束内部实际调用的是:

NSLayoutConstraint.activate(layoutConstraints)

这个方法我们比较熟悉,是系统添加约束的方法,只是activate方法做了一层封装,当然这个方法除了单纯添加约束也可作为更新约束使用,下面会详细讲解到。

image

上面这种图很形象的表示了约束执行的过程,在makeExtendableWithAttributes方法中,maker对象调用它的.bottom方法(类型为ConstraintMakerExtendable)将约束添加到descriptions数组中,返回ConstraintMakerExtendable类型进行下一次的链式调用,然后获取准备生产的Array<Constraint>,最后进行加工:

// maker对象调用makeConstraints方法,开始加工
func makeConstraints() {
    // 获得半成品
    let constraints = prepareConstraints(item: item, closure: closure)
    // 开始加工
    for constraint in constraints {
        constraint.activateIfNeeded(updatingExisting: false)
    }
}

我们继续看ConstraintMaker中的加工的机器:


func makeExtendableWithAttributes(_ attributes: ConstraintAttributes) -> ConstraintMakerExtendable {
    // 一条完整的约束描述类 类似 make.top.equal(10)
    let description = ConstraintDescription(item: self.item, attributes: attributes)
    //将单条约束添加到数组中
    self.descriptions.append(description)
    // 它可以实现多个属性链式操作 有了它 就可以实现 make.width.height 这种特性 
    // 其中width是ConstraintMaker的属性 height是ConstraintMakerExtendable的属性
    // 它们都是ConstraintMakerExtendable类型
    return ConstraintMakerExtendable(description)
}

有了这个ConstraintMakerExtendable类就可以通过链式调用比如.width方法,.height方法添加一些约束,来一步步完善ConstraintDescription类

ConstraintDescription可以看成是Constraint脚手架,在一步步添加约束时操作的都是ConstraintDescription类,等将所有约束添加到数组中,准备下一步生产时,会拿到它内部的constraint属性(Constraint类型)进行操作。

题外话,既然makeConstraints()方法内部执行了两步操作,那我们就可以利用这个特性,在视图有多种布局的时候,可以用到prepareConstraints方法,将布局提前装载好,然后根据状态执行不同显示效果,代码如下:

let v1 = View()
let v2 = View()
self.container.addSubview(v1)
self.container.addSubview(v2)

let constraints = v1.snp.prepareConstraints { (make) -> Void in
    make.edges.equalTo(v2)
    return
}

//打印 self.container.snp_constraints.count == 0,

for constraint in constraints {
    constraint.activate()
}

//打印 self.container.snp_constraints.count == 4,

for constraint in constraints {
    constraint.deactivate()
}

//打印 self.container.snp_constraints.count == 0,

再来讲讲 .equalTo()

其实和它类似的方法有很多包括:.equalToSuperview(), .lessThanOrEqualTo(), .lessThanOrEqualToSuperview(), .greaterThanOrEqualTo(), .greaterThanOrEqualToSuperview()实现的功能类似。相同点在是它内部调用的是同一个方法:

func relatedTo(_ other: ConstraintRelatableTarget, relation: ConstraintRelation, file: String, line: UInt) -> ConstraintMakerEditable {

这个方法返回一个ConstraintMakerEditable类型,用于对约束添加附加的操作(offset偏移量,priority优先级)。进入到这个方法内部,我们将核心代码提取出来:

func relatedTo(_ other: ConstraintRelatableTarget) -> ConstraintMakerEditable {
    if let other = other as? ConstraintItem {
        //这里处理参数类似于equalTo(view.snp.bottom)
    } else if let other = other as? ConstraintView {
        //这里处理参数类似于equalTo(view)
    } else if let other = other as? ConstraintConstantTarget {
        //这里处理参数类似于equalTo(50)
    } else if let other = other as? ConstraintLayoutGuide {
        //这里处理参数类似于equalTo(layoutGuide)
    }
}
let v1 = View()
let g1 = UILayoutGuide()

self.container.addSubview(v1)
self.container.addLayoutGuide(g1)
            
v1.snp.makeConstraints { (make) -> Void in
    make.top.equalTo(g1).offset(50)
    make.left.equalTo(g1.snp.top).offset(50)
}

如何实现make.top.equalTo(view)和make.top.equalTo(view.snp.top)效果一样?

通过查看ConstraintMakerRelatable类下面的relatedTo()方法,我们可以看到在传入不同类型的参数时(view和view.snp.top分别为UIView类型和ConstraintItem类型),方法内部经过处理,全部转化成了ConstraintItem处理,这时,我们猜想当参数类型是UIView时,是否自动转为了ConstraintItem类型,带着这个疑问我们接着看:

在relatedTo方法中关于视图的判断逻辑是这样子的:

if let other = other as? ConstraintView {
    related = ConstraintItem(target: other, attributes: ConstraintAttributes.none)
}

如果传入的是视图对象,则ConstraintItem对象的attributes就置为了.none,应该和这个有关,我们在来了解一下ConstraintAttributes是做什么的?

ConstraintAttributes

这是一个结构体类型,它遵守两个协议,通过这个字面量协议和多选协议,完成以组合的形式加入约束,最后通过layoutAttributes数组,对接到NSLayoutConstraint.Attribute中,下面我们来实现一个:

enum LayoutAttribute: CustomDebugStringConvertible {
    case left
    case right
    case top
    case width
    
    var debugDescription: String {
        switch self {
        case .left: return "左边"
        case .right: return "右边"
        case .top: return "上边"
        case .width: return "宽度"
        }
    }
}

struct ConstraintAttributes: OptionSet, ExpressibleByIntegerLiteral {

    typealias IntegerLiteralType = UInt

    var rawValue: UInt

    init(rawValue: UInt) {
        self.rawValue = rawValue
    }

    init(_ rawValue: UInt) {
        self.init(rawValue: rawValue)
    }

    init(integerLiteral value: IntegerLiteralType) {
        self.init(value)
    }

    static var none: ConstraintAttributes { return 0 }
    static var left: ConstraintAttributes { return 1 }
    static var right: ConstraintAttributes { return 2 }
}

extension ConstraintAttributes {
    var layoutAttributes:[LayoutAttribute] {
        var attrs = [LayoutAttribute]()
        if contains(ConstraintAttributes.left) { attrs.append(.left) }
        if contains(ConstraintAttributes.right) { attrs.append(.right) }
        if contains(ConstraintAttributes.none) { /*什么都不做*/ }
        return attrs
    }
}

在外面调用:

let attributes: ConstraintAttributes = [.left, .right]
let description = attributes.layoutAttributes.map{ $0.debugDescription }
print(description)
//["左边", "右边"]

另外我们还有一种简便的方式来实现ConstraintAttributes支持多选的方式:

struct ConstraintAttributes: OptionSet {
    
    var rawValue: Int
    
    init(rawValue: Int) {
        self.rawValue = rawValue
    }
    
    static var none = ConstraintAttributes(rawValue: 1 << 0)
    static var left = ConstraintAttributes(rawValue: 1 << 1)
    static var right = ConstraintAttributes(rawValue: 1 << 2)
}

我们在实现过程中没有使用字面量协议ExpressibleByIntegerLiteral,直接用的位于运算,和SnapKit作者实现的效果相同。

回到刚才的问题,如果在ConstraintItem构造方法(target: AnyObject?, attributes: ConstraintAttributes)中attributes传入.none,它表示对layoutAttributes数组不添加NSLayoutConstraint.Attribute元素。

因为SnapKit是对NSLayoutConstraint的封装,我们有必要说一说NSLayoutConstraint这个类了,它的构造方法如下:

/*
 item: 指定需要添加约束的视图一
 attribute: 指定视图一需要约束的属性
 relatedBy: 指定视图一和视图二添加约束的关系
 toItem: 指定视图一依赖关系的视图二;可为nil
 attribute: 指定视图一所依赖的视图二的属性,若view2=nil,该属性设置 NSLayoutAttributeNotAnAttribute
 multiplier: 系数
    情况一:设置A视图的高度 = A视图高度 * multiplier + constant;此时才会起作用;
    情况二:设置A视图和其他视图的关系或 toItem=nil,multiplier设置不等于0即可,若等于0会crash;
 constant: 常量
 
 layoutConstraint: 返回生成的约束对象
*/
NSLayoutConstraint(item view1: Any,
    attribute attr1: NSLayoutConstraint.Attribute,
    relatedBy relation: NSLayoutConstraint.Relation,
    toItem view2: Any?, 
    attribute attr2: NSLayoutConstraint.Attribute,
    multiplier: CGFloat, 
    constant c: CGFloat)

通过文档介绍说该方法实际上就是满足一个数学关系view1.attr1 = view2.attr2 * multiplier + constant

image

回到刚才的ConstraintItem类,对比NSLayoutConstraint的构造方法,仔细观察它就能发现,我们加的约束无非就是这样的关系:

owningView.ConstraintItem = view1 + attr1 
toView.ConstraintItem = view2 + attr2 

通过创建两个ConstraintItem就能完成基本的约束。了解这点就知道ConstraintItem的作用了。

通过查看源码发现在Constraint类的便利构造方法中,对NSLayoutConstraint进行了一层封装。包括equalTo(view.snp.top) 和 equalTo(view)实现一样的效果,都在这里做了逻辑处理。

最后再来介绍Constraint类

先来看这句代码

let constraint: Constraint = make.top.equal(20).constraint

我们经常在外面这样使用Constraint这个类,其中ConstraintDescription的作用是用于生产Constraint类,ConstraintDescription的创建在ConstraintMaker类的makeExtendableWithAttributes方法中。

实现一个动画效果

var constraint: Constraint?

view.snp.makeConstraints { (make) in
    constraint = make.top.equalToSuperview().offset(10).constraint
}

UIView.animateWithDuration(0.3, {
    constraint?.update(inset: 20)
    self.container.layoutIfNeeded
})

通常我们查看一个视图下面是否包含约束时,一般会直接调用view.constraints.isEmpty 来判断,严谨来讲某个视图下可能会包含一些约束,但这些约束是"不活跃的", 对视图显示不造成任何影响,所以判断条件需要改一下:

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

推荐阅读更多精彩内容