SnapKit源码解析

Snapkit是一个AutoLayout的封装库,是Masonary在Swift中的代替品。通过SnapKit,我们可以方便的进行UI的操作。

众所周知,苹果提供了AutoLayout方便了UI设计,但是官方提供的Api极度蛋疼, 为了一个四边相等,我们需要书写如下数据:

marqueeLabel.leftAnchor.constraint(equalTo: leftAnchor, constant: 0).isActive = true
marqueeLabel.rightAnchor.constraint(equalTo: rightAnchor, constant: 0).isActive = true
marqueeLabel.topAnchor.constraint(equalTo: topAnchor, constant: 0).isActive = true
marqueeLabel.bottomAnchor.constraint(equalTo: bottomAnchor, constant: 0).isActive = true

使用Snapkit之后,就可以改成如下数据

 marqueeLabel.snp.makeConstraints {(make) in
   make.edges.equalToSuperview()
 }

1、snp

ConstraintView+Extensions.swift

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

可以看到snp是ConstraintView扩展中提供的功能, ConstraintView本身是UIView的别名

涉及类:ConstraintView、ConstrainDSL

1.1、ConstraintView

ConstraintView是外观模式的一个标准应用,通过别名,它混一了iOS的UIView与MacOS的NSView。

#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

继承协议:ConstraintRelatableTarget

1.1.1、ConstraintRelatableTarget

Snapkit库中,对外观模式的应用是十分值得学习的地方。
在设置约束的过程中,会使用多种类型的数据:

make.edges.equalTo(view1.snp.bottom)
make.edges.equalTo(view1)
make.size.equalTo(10)
make.size.equalTo(CGPoint(x: 10, y: 10))

可以看到,equalTo()函数的参数存在许许多多的数据类型,Snapkit通过协议ConstraintRelatableTarget对其进行了方便的封装。

public protocol ConstraintRelatableTarget {
}

extension Int: ConstraintRelatableTarget {
}

......

extension ConstraintInsets: ConstraintRelatableTarget {
}

extension ConstraintItem: ConstraintRelatableTarget {
}

extension ConstraintView: ConstraintRelatableTarget {
}
ps:

UIView在Snapkit中的别名:

结构体:ConstraintView,
协议: LayoutConstraintItem,添加各种约束
包装者:ConstraintItem,将View与Attribute混为一体

2、makeConstraints

 marqueeLabel.snp.~~**makeConstraints**~~

updateConstraints,removeConstraints,makeConstraints 这一系列方法就不展开了。

顾名思义,makeConstraints函数中生成了NSLayout的约束。

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

可以看出,ConstraintViewDSL中的makeConstraints是对ConstraintMaker的makeConstraints进行的封装。

涉及类 ConstraintViewDSL,ConstraintMaker

2.1、ConstraintViewDSL

image.png

本类的初始化需要ConstraintView,也就是marqueeLabel
封装了prepareConstraints,makeConstraints,remakeConstraints,updateConstraints,removeConstraints等 一系列处理方法。它们统一都是调用ConstraintMaker类的静态同名方法,只是将ConstraintView作为第一个参数统一进去。

继承类:ConstraintAttributesDSL,ConstraintBasicAttributesDSL

2.1.1、ConstraintAttributesDSL,ConstraintBasicAttributesDSL

涉及类 ConstraintItem

这两个类可以放在一起说,它们都是提供了生成ConstraintItem的数据。

public var left: ConstraintItem {
   return ConstraintItem(
     target: self.target, 
     attributes: ConstraintAttributes.left
   )
}

可以猜出,这里是和

make.left.equalToSuperview()

相关的,至于如何使用,我们会在生成约束的地方来讲。

2.1.2、ConstraintItem
image.png

ConstraintItem很好理解,就是将目标(target)和约束 (attributes)绑定在一起。

可以看到target是AnyObject,这是因为target不一定是UIView.下面我们讲到Constraint类时,会发现Constraint的from,to属性都是ConstraintItem,target在from中一般是一个UIView,而在to中,由于有make.width.equalTo(100)这种属性存在,ConstraintItem可能没有target。

3、ConstraintMaker

ConstraintMaker 是Snapkit的核心类,在本类中生成了约束

让我们来看一下makeConstraints的定义:(展开版)

 func makeConstraints(item: LayoutConstraintItem, closure: (_ make: ConstraintMaker) -> Void) {
 ####第一部分
    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)
    }
    for constraint in constraints {
        constraint.activateIfNeeded(updatingExisting: false)
        }
    }

本类主要分成了生成约束描述与约束描述翻译两个部分:

3.1、生成约束描述

第一部分:约束定义,通过makeExtendableWithAttributes生成一系列ConstraintMakerExtendable 型数据:

关键类 ConstraintDescription
**涉及类 ConstraintAttributes, ConstraintDescription,ConstraintMakerExtendable,ConstraintMakerRelatable **

1. private var descriptions = [ConstraintDescription]()
 
2. public var left: ConstraintMakerExtendable {
        return self.makeExtendableWithAttributes(.left)
    }
    
3. func makeExtendableWithAttributes(_ attributes: ConstraintAttributes) -> ConstraintMakerExtendable {
        let description = ConstraintDescription(item: self.item, attributes: attributes)
        self.descriptions.append(description)
        return ConstraintMakerExtendable(description)
    }
    

由此可知


marqueeLabel.snp.makeConstraints {(make) in
  ~~** make.left.equalToSuperview()**~~
 }
 
{(make:ConstraintMaker) in
    let maker = ConstraintMaker(item: item)
    let description = ConstraintDescription(
        item: marqueeLabel,
        attributes: ConstraintAttributes.left
    )
    self.descriptions.append(description)
    let extend : ConstraintMakerExtendable = 
    ConstraintMakerExtendable(description) 
    
    extend.equalToSuperview()
}

3.1.1 ConstraintAttributes

在此类中,封装了 Snapkit 到 LayoutAttribute 的转换

**继承协议 OptionSet, ExpressibleByIntegerLiteral **

通过这两个协议,使得ConstraintAttributes可以通过 Int,Int数组,枚举,枚举数组等各种形式初始化.可以通过container决定是否存在数据。

在ConstraintAttributes中,提供了一系列left,right之类的静态成员。

static var left: ConstraintAttributes {
    return 1 
}

LayoutAttribute类似ConstraintView,也是通过外观模式混一了iOS,MacOS,多种版本,可以简单的对标成NSLayoutConstraint。

var layoutAttributes:[LayoutAttribute] 就是协议NSLayoutConstraint的数组。


var layoutAttributes:[LayoutAttribute] {
    var attrs = [LayoutAttribute]()
    if (self.contains(ConstraintAttributes.left)) {
            attrs.append(.left)
        }
        if (self.contains(ConstraintAttributes.top)) {
            attrs.append(.top)
        }
}

可以看到,根据自身是否含有静态成员,NSLayoutAttribute存储到了ConstraintAttributes的layoutAttributes属性中。

也就是说


ConstraintAttributes.left 中存在一个

marqueeLabel.leftAnchor

3.1.2 ConstraintDescription

ConstraintDescription 是约束的描述者,
约束的生成过程,就是从
make.top.lessThanOrEqualTo(imageView.snp.bottom).offset(20).multipliedBy(2).priorityLow()
这样的语句转成ConstraintDescription的过程

 let description = ConstraintDescription(
        item: marqueeLabel,
        attributes: ConstraintAttributes.left
    )
self.descriptions.append(description)

可以看出ConstraintDescription通过item与ConstraintAttributes初始化,

来看下的类图

image.png

对其中的各个属性,我们来一一解释:

item: LayoutConstraintItem
attribute: ConstraintAttributes
这两个属性在初始化时写入

relation: ConstraintRelation
一个简单的枚举,对应到 NSLayoutConstraint.Relation 的三种状态

case .equal:
   return .equal
case .lessThanOrEqual:
   return .lessThanOrEqual
case .greaterThanOrEqual:
   return .greaterThanOrEqual

对应了 lessThanOrEqualTo

related: ConstraintItem
ConstraintItem包裹LayoutConstraintItemConstraintAttributes,用来判断是否数据是否一致

对应了 imageView.snp.bottom

multiplier: ConstraintMultiplierTarget
ConstraintMultiplierTarget将Int,UInt,Float,CGFloat等一系列数据转换为CGFloat

对应.multipliedBy(2)

constant: ConstraintConstantTarget
这个需要结合ConstraintInsetTarget : ConstraintConstantTarget来看
ConstraintConstantTarget将Int,UInt,Float,CGFloat, UIEdgeInsets, CGPoint, CGSize 等一系列数据转换为需要的约束值CGFloat:

extension ConstraintConstantTarget {
    
    internal func constraintConstantTargetValueFor(layoutAttribute: LayoutAttribute) -> CGFloat 

对应.offset(20)

priority: ConstraintPriorityTarget
ConstraintPriorityTarget,UInt,Float,CGFloat等一系列数据转换为UILayoutPriority

对应.priorityLow()

constraint: Constraint?
这是一个懒加载的数据,对约束的操作都放在这个里面进行,我们到最后再来讲解

当前数据只写入了前两个成员变量,之后成员变量如何写入会慢慢介绍。

3.1.3 ConstraintMakerExtendable

**继承类 ConstraintMakerRelatable **

本方法提供的是链式调用

make.left.right.top.....

make.left 我们已经知道,等价于生成一个ConstraintMakerExtendable

let ext: ConstraintMakerExtendable = self.makeExtendableWithAttributes(.left)

而后续的.right.top.....则是

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

let ext: ConstraintMakerExtendable = self.makeExtendableWithAttributes(.left)
ext.description.attributes += .right
ext.description.attributes += .top

3.1.4 ConstraintMakerRelatable

提供 equalTo, equalToSuperview, ....等一系列方法

**继承类 ConstraintMakerEditable **

这一系列方法都是基于 relateTo 函数

之前我们说过在

 let description = ConstraintDescription(
        item: marqueeLabel,
        attributes: ConstraintAttributes.left
    )
self.descriptions.append(description)

此时description还只填写了前两个成员变量。

在relatedTo 函数根据other与relation的类型 ,填写description中的数据:


image.png
closure 类型 生成数据
top.equalTo(a.snp.bottom) ConstraintItem related = other,constant = 0.0
top.equalToSuperview() ConstraintView related = ConstraintItem(target: other, attributes: ConstraintAttributes.none),constant = 0.0
height.equalTo(65) ConstraintConstantTarget related = ConstraintItem(target: nil, attributes: ConstraintAttributes.none),constant = other
   let editable = ConstraintMakerEditable(self.description)
        editable.description.sourceLocation = (file, line)
        editable.description.relation = relation
        editable.description.related = related
        editable.description.constant = constant

3.1.5 ConstraintMakerEditable

**继承类 ConstraintMakerPriortizable **

提供 multipliedBy, dividedBy, offset, inset等一系列方法, 设置常量数据, 填充了ConstraintDescription中如下的数据:

image.png

至此,我们可以知道

marqueeLabel.snp.makeConstraints {(make) in
  ~~** make.left.equalToSuperview()**~~
 }
 
{(make:ConstraintMaker) in
    let maker = ConstraintMaker(item: item)
    let description = ConstraintDescription(
        item: marqueeLabel,
        attributes: ConstraintAttributes.left
    )
}

这段代码的目标就是生产一个ConstraintDescription,然后将其存储在ConstraintMaker的descriptions数组中中。

marqueeLabel.snp.makeConstraints {(make) in
  ~~** make.left.equalToSuperview()**~~
   make.right.equalToSuperview()
    make.top.equalToSuperview()
     make.bottom.equalToSuperview()
 }

随着block中数据的增多,descriptions逐渐被填满。下面一步就是用descriptions来生成对应的约束了。

3.2、约束描述翻译

涉及类: Constraint
上面我们说的ConstraintDescription,还有最后一个成员变量 constraint: Constraint?,这个变量就是有desc翻译出来的约束,这这个类中,我们最终将descriptions转义成NSLayoutConstraint

3.2.1、Constraint

涉及类:ConstraintItem
Constraint是ConstraintDescription到NSLayoutConstraint中转站,其初始化需要如下参数:

变量名 变量类型 描述
from ConstraintItem 约束将实现在那个view上,这个view上有哪些约束条件
to ConstraintItem 约束相关的View或者状态
relation ConstraintRelation 约束关系,equal,less,greater
multiplier ConstraintMultiplierTarget 约束比例
constant ConstraintConstantTarget 约束值
layoutConstraints LayoutConstraint 约束值

本类所做的就是将约束关系转换成为所要布局的视图和对应的布局视图的位置关系。

然后,统一对每个NSLayoutConstraint执行activate,其本质是调用activateIfNeeded函数.

func activateIfNeeded(updatingExisting: Bool = false) {
        let layoutConstraints = self.layoutConstraints
        if updatingExisting {
           updateExistData()
        } else {
            NSLayoutConstraint.activate(layoutConstraints)
            item.add(constraints: [self])
        }
    }

到这一步,基本的约束就已经构建成功了。

学到的东西

  1. 通过协议使用的外观模式
public protocol ConstraintRelatableTarget {
}

extension Int: ConstraintRelatableTarget {
}

......

extension ConstraintInsets: ConstraintRelatableTarget {
}

extension ConstraintItem: ConstraintRelatableTarget {
}

extension ConstraintView: ConstraintRelatableTarget {
}
  1. 通过别名使用的外观模式
#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. 不使用库时 AutoLayout的写法
////www.greatytc.com/p/d67395deb694
view.translatesAutoresizingMaskIntoConstraints = false //自动把frame转换成约束,可能导致冲突,需要关闭

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