SnapKit源码解析

简介

什么是Snapkit

  • SnapKit是一个使用 Swift 编写而来的AutoLayout框架,通过使用Snapkit,我们可以通过简短的代码完成布局,如下所示:

原生布局

contentView.addSubview(imageView)
imageView.translatesAutoresizingMaskIntoConstraints = false

addConstraint(NSLayoutConstraint(item: imageView,
                                 attribute: .leading,
                                 relatedBy: .equal,
                                 toItem: contentView,
                                 attribute: .leading,
                                 multiplier: 1,
                                 constant: 0))

addConstraint(NSLayoutConstraint(item: imageView,
                                 attribute: .top,
                                 relatedBy: .equal,
                                 toItem: contentView,
                                 attribute: .top,
                                 multiplier: 1,
                                 constant: 0))

addConstraint(NSLayoutConstraint(item: imageView,
                                 attribute: .trailing,
                                 relatedBy: .equal,
                                 toItem: contentView,
                                 attribute: .trailing,
                                 multiplier: 1,
                                 constant: 0))

addConstraint(NSLayoutConstraint(item: imageView,
                                 attribute: .bottom,
                                 relatedBy: .equal,
                                 toItem: contentView,
                                 attribute: .bottom,
                                 multiplier: 1,
                                 constant: 0))

SnapKit布局:

contentView.addSubview(imageView)

imageView.snp.makeConstraints { make in
    make.edges.equalTo(contentView)
}
  • DSL(Domain specific Language)特定领域语言
    DSL是为了解决某些特定场景下的任务而专门设计的语言。如果能把一些设计师产出的长宽、色值、文字、居中、距上等设计元数据(设计的标注信息等),以一种约定的简洁的语言规则(即DSL)输入给程序代码,由程序和代码自动的分析和处理,从而生成真正的界面开发代码setFrame,setTitle,setColor,addSubview,这样就可以大幅度的减少代码量与工作量,程序员来写这种简洁的语法规则会更快更高效,甚至可以把这种简洁的语法规则教会设计师,让设计师有能力直接写出DSL,然后输入给底层程序,这样界面就自然完成。

注意事项

  • 使用SnapKit前,一定要先将子控件添加到父视图中,否则会直接崩溃!
parentView.addSubview(subview)
  • leading和left、trailing和right

其实在目前国内App中使用leading与left,trailing与right在正常情况下是等价的,这是因为国内的阅读习惯是从左到右的,不过如果你的App需要在阿拉伯国家上架,他们的布局是从右至左时(比如阿拉伯文) 则会对调。

建议使用leading和trailing,便于App国际化。

使用教程

  • 较为简单,api也不多,不多描述了

源码解析

详细解析

  • snp

lable.snp通过给view加扩展实现的

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

snp 最后是生成了一个 ConstraintViewDSL 对象

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

这里tvOS是基于 iOS的操作系统,tvOS 是专门为第四代 Apple TV设计的操作系统。

  • ConstraintViewDSL
internal init(view: ConstraintView) {
     self.view = view 
}

ConstraintViewDSL 类的构造函数,就是将 view 保存起来

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

makeConstraints 函数将传进来的闭包传递给ConstraintMaker 这个类去处理了

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

该方法主要调用了被接受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,然后调用闭包,闭包内部会添加一些约束,接下来就是获取这些约束, 最后将约束激活。

闭包就是能够读取其他函数内部变量的函数。例如在程序中,只有函数内部的子函数才能读取局部变量,所以闭包可以理解成“定义在一个函数内部的函数“在本质上,闭包是将函数内部和函数外部连接起来的桥梁。

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

这是ConstraintMaker的构造函数,这里出现了一个新的类型LayoutConstraintItem,表示一个可布局的对象。

public protocol LayoutConstraintItem: class {
}

可以看到这是一个协议

extension ConstraintLayoutGuide : LayoutConstraintItem {
}
extension ConstraintView : LayoutConstraintItem {
}

ConstraintView 和 ConstraintLayoutGuide 都实现LayoutConstraintItem这个协议。

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

该协议实现了一些方法,包含prepare方法。这一步其实就是禁用 View 的 AutoresizeMask。

回到开始的闭包,里面我们写的make.center.equalTo(self.view.snp.center)可以通过这个函数生成一些约束对象。首先我们都知道, 每一个约束, 首先需要添加到一个对象上面, 还需要约束的属性,关系大于、等于、小于,如果不是常量类型,还需要另一个依赖的对象,以及依赖的属性,系数以及一个偏移常量。

这里的 make.center就是说添加到当前,并设置约束属性center,equalTo,则是表示关系为等于,self.view.snp.center则表示依赖的对象是 self.view,依赖的属性也是 center,系数及偏移值这里均没有指定,表示使用默认值。

public var center: ConstraintMakerExtendable {
        return self.makeExtendableWithAttributes(.center)
}

这个只是一个简便方法, 具体的实现继续去查看定义

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

流程为首先根据约束属性及需要添加约束的对象生成一个描述,然后将其添加内部的一个数组,也就是之前 makeConstraints中第一个 for 循环锁遍历的数组,最后返回一个 ConstraintMakerExtendable 对象。

  • ConstraintAttributes
internal struct ConstraintAttributes : OptionSet, ExpressibleByIntegerLiteral {
}

ConstraintAttributes 本身是一个 OptionSet

public protocol OptionSet : RawRepresentable, SetAlgebra {
}
extension RawRepresentable where Self : Encodable, Self.RawValue == String {
    public func encode(to encoder: Encoder) throws
}
extension RawRepresentable where Self : Decodable, Self.RawValue == String {
    public init(from decoder: Decoder) throws
}

初始化,成为统一可操作的类型。

internal struct ConstraintAttributes : OptionSet {
    internal private(set) var rawValue: UInt internal init(rawValue: UInt) { self.rawValue = rawValue
    }
    internal static var left: ConstraintAttributes { 
        return self.init(1) 
    }
    internal static var top: ConstraintAttributes { 
        return self.init(2) 
    }
    internal static var right: ConstraintAttributes { 
        return self.init(4) 
    }
    ...这里有省略
    internal static var center: ConstraintAttributes { 
        return self.init(768) 
    }
}

ConstraintAttributes 本身是一个 OptionSet,里面定义了许多属性, 例如 left, right, center使用 OptionSet 的意义在于,可以通过组合操作,同时添加多个属性,例如,center这个属性就是由 centerX 和 centerY 复合而来。

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? =
    ...
    internal init(item: LayoutConstraintItem, attributes: ConstraintAttributes){
    self.item = item
    self.attributes = attributes
}

这个类是一个描述类,用于描述一条具体的约束,里面包含了约束的属性,关系等回到ConstraintMaker.makeConstraints 中的第一个 for 循环,里面就是去获取description.constraint 已达到最终构造约束的目的。

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

makeExtendableWithAttributes最后返回的时候, 返回的是一ConstraintMakerExtendable对象。这个类的主要目的是为了实现链式的多属性,例如,make.center.equalTo(self.view.snp.center)这一句可以写为,make.centerX.centerY.equalTo(self.view.snp.center)

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

ConstraintMakerExtendable 继承自 ConstraintMakerRelatable,这个类主要是负责构造一个关系,例如 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? UIView {
                 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
}         // equalTo 只是对内部函数relatedTo 的一个简单调用
public protocol ConstraintRelatableTarget {
}
extension Int: ConstraintRelatableTarget {
}
extension UInt: ConstraintRelatableTarget {
}
extension Float: ConstraintRelatableTarget {
}
extension ConstraintItem: ConstraintRelatableTarget {
}
extension ConstraintView: ConstraintRelatableTarget {
}

ConstraintRelatableTarget是一个协议,表示一个可以被依赖的目标,我们在手写 NSLayoutConstraint 的时候,
依赖对象可以为 view,可以为ConstraintLayoutGuide,也可以为空,为空的时候,表示使用绝对值,该协议分别有 Int、 Double、CGPoint等字面值,也有UIView, ConstraintLayoutGuide,同时,也有ConstraintItem,让我们可以指定依赖的具体值, 我们之前的代码 make.center.equalTo(self.view.snp.center)中的self.view.snp.center就是 ConstraintItem对象。

  • ConstraintItem

view.snp返回的是一个 ConstraintViewDSL,ConstraintViewDSL是继承自 ConstraintAttributesDSL,而ConstraintAttributesDSL则是继承自 ConstraintBasicAttributesDSL的ConstraintAttributesDSL与 ConstraintBasicAttributesDSL中定义了大量的布局属性,如 top, bottom 等

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

其他均类似。可以看到这里面构造了一个 ConstraintItem 对象:

public final class ConstraintItem {
    internal weak var target: AnyObject?
    internal let attributes: ConstraintAttributes
    internal init(target: AnyObject?, attributes: ConstraintAttributes) {
        self.target = target
        self.attributes = attributes
     }
     internal var layoutConstraintItem: LayoutConstraintItem? {
          return self.target as? LayoutConstraintItem
     }
}
  • ConstraintMakerEditable

ConstraintMakerEditable 这个类主要是设置Autolayout 中的两个常量multiplier 和 constant 与优先级,使用方法如make.center.equalTo(self.view.snp.center).offset(20)

再次回到makeConstraints,通过上面的若干步骤,完成了对 ConstraintDescription的设置,现在可以用他来生成 Constraint了,生成的部分在ConstraintDescription 的 constraint 属性里面

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

Constraint 创建过程很像NSLayoutConstraint
Constraint这个类主要就是生成和操纵 NSLayoutConstraint。构造函数有点长,下面是去掉一些简单的赋值和多平台适配后的代码

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

函数中第一行的self.layoutConstraints = []使用来存放所有最后生成的NSLayoutConstraint
后面的两行是获取两个对象的约束属性。而 layoutFrom则是约束属性的起始对象,在我们最初那段代码中,就表示了snplabel这个视图。

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

后面则是获取约束的关系, 如等于, 大于。主要的代码都在那个循环中,主要逻辑是遍历添加在起始对象上的约束属性,然后获取预支对应的目标对象及目标对象的约束属性,最后生成LayoutConstraint

其中第一个 if else 分支中在确定目标属性该使用何种值, 通过分析可以看出, 我们之前那段代码, 其实可以将make.center.equalTo(self.view.snp.center)中直接写为make.center.equalTo(self.view)

后面则是根据不同的目标属性,获取适当的偏移值。以及获取目标对象。
后面 LayoutConstraint(xxx) 中的 LayoutConstraint 其实只是一个NSLayoutConstraint 的子类,只是在其中添加了一个标签与创建者(Constraint) 的引用

  • activateIfNeeded

makeConstraints最后一步则是激活, 在 iOS 8 以前, 所有的依赖属性, 都必须使用 view.addConstraint(xxx)方法将依赖激活, iOS 8 后, 则直接将依赖激活即可生效。activateIfNeeded 则是将依赖激活使其生效

SnapKit 源码结构图

结构图.png

SnapKit 源码类图

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

推荐阅读更多精彩内容