Snapkit源码阅读浅析

前言

SnapKit库的使用非常简单,它受到很多开发者的喜爱和使用。那么,我们知道了怎么使用它后,有没有想过它的源码是什么样子呢?现在,就让我们来看一下SnapKit的“内心世界”到底是怎样的五彩斑斓,才能够如此引起开发者们的垂涎和追捧?

SnapKitgithub链接: 猛戳这里

SnapKit文件结构

SnapKit总共有34个文件, 每个文件都有各自的功能,文件之间又相互关联,起到链接的作用。它主要是基于系统API的NSLayoutConstraint类的一个封装, 将系统API的类、方法、属性、协议、结构体、枚举等整合起来,形成一条条独立、简洁、清爽、易用的布局链。

SnapKit Files.png

从上面图片中的列表可以看到,总的有34个文件,现在,你是否会一下子就感觉到头疼,这么多文件,那是不是很复杂?代码是不是特别多?一想到这里就头疼死了。

实际上,尽管SnapKit有那么多的文件,但是它们都相对来说比较简单,文件结构十分清晰,除了Constraint.swift文件超过了200行代码,其他基本都是几行到几十行不等,有些文件只有一个协议方法,甚至有好几个文件都只是定义一个别名而已,例如:ConstraintInsets.swiftConstraintLayoutGuide.swiftConstraintLayoutSupport.swiftConstraintView.swift等。

struct1.png
struct2.png

以上两张图几乎是SnapKit牵扯到的ClassEnumStructprotocol等结构,至于继承系统类或者器别名的没有写出来。事实上,为了更好地增强SnapKit的可读性和关联性,作者几乎对用到的系统API都做了封装。

SnapKit之snp浅析

为什么要说snp? 它有何特别之处?对于SnapKit布局来说,有什么作用?

因为在SnapKit布局中,我们一定且必须使用到snp, 先看下面的布局代码

let subview = UIView()
subview.backgroundColor = .purple
view.addSubview(subview)
subview.snp.makeConstraints { (make) in
    make.width.equalTo(100)
    make.height.equalTo(200)
    make.center.equalToSuperview()
}

在布局中,snp是当前所要布局view的一个延展属性, 在SnapKit源码中有

typealiasUIView.png
extensionUIView.png

说明:在ConstraintViewextension中,还有很多已经废弃了的方法、属性,作者用了上面这个snp来代替,主要是因为实现代码更加swift化,也更加方便管理。

从本文中的文件结构的图片中可以看出,snp是一个结构体实例对象,结构体ConstraintViewDSL继承于ConstraintAttributesDSL协议。而在snp所属的结构体中,有着几个我们制作约束时所需要的实例方法和属性, 通过view.snp.的形式,我们就可以开始进行调用ConstraintViewDSL结构体的方法进行布局了,这是布局开始的第一步,也很好理解。

SnapKit之makeConstraints{}方法的浅析

继上面一个分析后,我们知道,在布局开始时,制作约束,必须调用ConstraintViewDSL结构体的方法来对视图进行布局,那我们就来看一下这个方法(已经把其他方法屏蔽,便于分析)

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结构体实例后,从上面代码段中可以看出,我们就可以调用makeConstraints方法了。

在调用ConstraintViewDSL结构体的实例方法makeConstraints的时候,我们实质上是对ConstraintMaker 类的静态方法makeConstraints的调用,这里主要是做了一个回调而已。

这里面有一个ConstraintViewDSL结构体的初始化方法,用来保存当前的视图,也就是在我们调用subview.snp的时候, 就已经初始化结构体并且引入当前的视图

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

那么为何要引入当前的视图呢?原因是我们在调用制作约束makeConstraints的时候需要使用到当前的视图, 也就是下面这句代码中的view

ConstraintMaker.makeConstraints(item: self.view, closure: closure)

调用方式就是

subview.snp.makeConstraints { (make) in
    
}

既然调用subview.snp.makeConstraints实质上是调用ConstraintMaker类的静态方法makeConstraints来进行布局,那么现在,我们进入到ConstraintMaker类文件中来分析它的静态方法makeConstraints,看看它是如何操作布局的

ConstraintMaker类的makeConstraints布局浅析

ConstraintMaker类中:

makeConstraints.png

然后我们可以传入参数第一个是item, 是一个LayoutConstraintItem, 它是一个类协议。因为ConstraintView继承这个协议,所以刚才在ConstraintViewDSL结构体makeConstraints方法中,我们传入了当前的视图, 即在这里, item就是我们传入的当前的view

我们再来看makeConstraints中, 有这两行代码

 let maker = ConstraintMaker(item: item)
 closure(maker)

这里为何要使用let maker = ConstraintMaker(item: item), 原因是我们在调用当前类的makeConstraints方法时,直接直接对当前类进行初始化,在获取到ConstraintMaker的实例的同时,也对ConstraintMaker的私有属性private let item: LayoutConstraintItem进行了初始化,如下

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

为何要获取ConstraintMaker的实例的原因是我们需要closure(maker), 也就是实例对象,这样的话,我们就可以在外面布局的时候通过make.widthmake.right等来获取到ConstraintMakerwidthheightleftright等属性。

接着看makeConstraints中, 有

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

首先是定义了约束的数组constraints, 数组中的元素Constraint是一个约束类,你先别管里面有什么,怎么实现,你现在只要知道它是约束,并且是一个类,现在被放到一个数组中,其他的我们后面会讲到。

接着,遍历我们刚刚初始化的类实例的属性descriptions, 也许有人就有疑问了,private var descriptions = [ConstraintDescription]()我根本就没有初始化过,哪来的值呢?好的,别急,等下再告诉你,你只要知道它是一个ConstraintDescription约束描述类, 遍历之后我们就可以拿到ConstraintDescription约束描述类的约束属性constraint了,但是它是一个可选类型,所以我们必须拆包处理,故用guard语句判断,如果约束存在,则往所定义的约束数组中添加约束, 不存在,退出本次循环,继续判断约束是否存在,直到所有的约束都添加完毕为止。

最后,遍历刚刚添加的约束数组,并且激活所有的约束。这个activateIfNeeded(updatingExisting: false)约束类Constraint中的激活约束的方法,为什么这个里面的参数updatingExisting为何传的是false而不是true呢?因为这是一个makeConstraints方法,你给一个视图才开始布局,就需要更新已经存在的约束吗?才刚开始制作约束,哪来的“已经存在”的约束呢?所以传true的时候是针对updateConstraints方法的。 具体细节待我一一道来。

现在我们在刚才的分析中,存在三个疑点

  • 疑点一,maker.descriptions是约束描述类,但未初始化,我们却使用了?

  • 疑点二,Constraint是一个约束类,怎么实现的,为何要在这里创建、添加、激活?

  • 疑点三,activateIfNeeded怎么实现?这里为何要使用它来激活约束?

ConstraintDescription约束描述类的布局浅析

为了解释上面我们提到的几个疑点,我们首先来看一下这个ConstraintDescription约束描述类是一个什么东西。为何不讲Constraint类呢?不是不讲,现在我们先知道它就是一个约束类,在刚才的makeConstraints方法中,它是一个数组的元素,我们现在是在添加元素,而添加的元素恰好是对maker.descriptions的一个遍历,所以,我们会先来看ConstraintDescription约束描述类,再看Constraint约束类

先来看其源码

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

好,刚才在ConstraintMaker中是不是有一个private var descriptions = [ConstraintDescription]()私有属性呢?是的,但是,我们使用它了,却未见将它初始化,没有初始化,意味着没有值。实际上它在ConstraintMaker中已经初始化了,我们来看里面的这个方法

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

首先是对ConstraintDescription约束描述类进行了初始化,然后得到实例对象description, 然后self.descriptions.append(description)。至于这个makeExtendableWithAttributes是干嘛的?别急,下面会讲到,这里只要知道ConstraintDescription约束描述类进行了初始化并且maker.descriptions有值了就好了。

ConstraintDescription约束描述类的源码中可以看出,它有一个constraint属性,也就是Constraint约束类的一个实例对象,好了,这样就清楚了为何这里可以遍历,并且放进一个刚创建的var constraints: [Constraint] = []数组中。

分析一下ConstraintDescription约束描述类的初始化方法,即

 internal init(item: LayoutConstraintItem, attributes: ConstraintAttributes) {
        self.item = item
        self.attributes = attributes
 }

传入了参数item, 就是之前提到的当前view, 另外一个是布局属性,也就是常常使用的widthheightleadingright等等。

分析一下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
    )
}()

这个是一个懒加载方法,并且是可选的,开始用guard语句判断了三个可选类型,分别是self.relationself.relatedself.sourceLocation。这里可能有人又有疑惑了,这三个属性并未看见其初始化,怎么又直接使用了呢?并且,他们是什么东西?有什么作用呢?好,其实这个又跟ConstraintMaker类中的方法makeExtendableWithAttributes返回值(实例对象)ConstraintMakerExtendable有关,暂且不说,只要知道这些属性只要我们调用make.width.equalTo的时候就已经被初始化就好,因为用户不一定会正确地传入值,所以,有可能有值,有可能没值,故用可选类型定义,并且判断三个条件必须符合之后,才能制作约束。解释一下三个属性的作用:

  • self.relation

枚举类型,就是对系统的NSLayoutRelation的封装。即枚举值是

public enum NSLayoutRelation : Int {

    case lessThanOrEqual

    case equal

    case greaterThanOrEqual
}
  • self.related

是一个ConstraintItem类实例对象,其实就是我们传入的所要布局的目标视图。

  • self.sourceLocation

资源定位。是一个元组,(String,Int)类型,也就是出错的时候定位在(#file,#line)哪个文件,哪一行。

接着,在判断之后,创建了开始布局的视图from, 返回Constraint约束类的初始化后的实例对象。

到此为止,刚才牵扯到的疑问已经解决其中之一,还有两个疑问,实际上是和Constraint约束类相关。那么接下来,我们就来讲一下Constraint约束类

Constraint约束类的布局浅析

这个类是非常重要的一个类,我把它当做SnapKit的核心类,因为布局就牵扯到约束,没有约束的布局一切都是虾扯蛋的。

我们先来回答刚才的一个疑问,就是在调用makeConstraints制作约束的时候,为何要调用

constraint.activateIfNeeded(updatingExisting: false)

为了快速回答这个问题,我们直接定位到Constraint约束类的方法

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

因为之前我们穿的是Bool值是false, 所以我们直接定位到else部分

NSLayoutConstraint.activate(layoutConstraints)
item.add(constraints: [self])

这样就很显而易见了,万变不离其宗, 调用系统API激活约束。那么又问又有疑惑了,激活约束后怎么还有item.add(constraints: [self])呢?这很奇怪呀!!!好,我来回答你的疑惑,这句代码目的是为了保存约束,方便你可以做移除约束的操作,如果你不信,你可以在源码中将这行代码删掉,你的约束也是正常的,并且视图也会正确显示!但是,当你调用snp.removeConstraints()方法的时候,不起任何作用!!!调用snp.remakeConstraints就会造成约束冲突,视图变形!

最后一个疑问,Constraint约束类的作用,这个我们直接来看初始化方法

    // MARK: Initialization

    internal init(from: ConstraintItem,
                  to: ConstraintItem,
                  relation: ConstraintRelation,
                  sourceLocation: (String, UInt),
                  label: String?,
                  multiplier: ConstraintMultiplierTarget,
                  constant: ConstraintConstantTarget,
                  priority: ConstraintPriorityTarget) {
        self.from = from
        self.to = to
        self.relation = relation
        self.sourceLocation = sourceLocation
        self.label = label
        self.multiplier = multiplier
        self.constant = constant
        self.priority = priority
        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: LayoutAttribute
            #if os(iOS) || os(tvOS)
                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
                    }
                }
            #else
                if self.from.attributes == self.to.attributes {
                    layoutToAttribute = layoutFromAttribute
                } else if layoutToAttributes.count > 0 {
                    layoutToAttribute = layoutToAttributes[0]
                } else {
                    layoutToAttribute = layoutFromAttribute
                }
            #endif

            // 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 = LayoutPriority(rawValue: self.priority.constraintPriorityTargetValue)

            // set constraint
            layoutConstraint.constraint = self

            // append
            self.layoutConstraints.append(layoutConstraint)
        }
    }

其实这个也没多少需要详细讲得,无非就是先初始化属性,然后判断所要布局的视图和对应的布局视图的位置关系,这里举一个与父视图的关系,比方说,现在我刚创建一个Xcode工程,并且创建了一个子视图,我们想要让所要布局的子视图的edges等于父视图的margins, 那么我们可以这样写:

subview = UIView()
subview.backgroundColor = .purple
view.addSubview(subview)
subview.snp.makeConstraints { (make) in
    make.edges.equalTo(view.snp.margins)
}

这非常简单,然后约束调用的是这个判断

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

为什么呢?原因是self.from.attributes == .edges && self.to.attributes == .margins条件满足,即布局视图的attributes就是edges, 等于父视图的margins, 从这句代码make.edges.equalTo(view.snp.margins)可以很好地看出来。不清楚的话,可以将代码“照葫芦画瓢”试试。一定会调用的。

最后就添加布局约束self.layoutConstraints.append(layoutConstraint)就搞定了。

一切基本讲得差不多了,但是,我们还有一个东西要讲,那就是关系属性make.top.right.left.trailing.leading.width.edges等等, 往下看

ConstraintMakerExtendable类的布局浅析

要讲这个的目的是,在SnapKit中,所有布局我们都要使用make.widthmake.right等的形式,而后面的这些宽、高、左边、右边、前边、后边、边缘等都是ConstraintMakerExtendable类的实例对象。

ConstraintMakerExtendable类继承于ConstraintMakerRelatable类,在ConstraintMakerExtendable类中,都是这种形式

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

这里就是创建了一个自身的实例对象,返回自身的一个实例。关键点在于self.description.attributes += .top, descriptionConstraintMakerRelatable的一个属性,因为继承的关系,所以这里有self.description, attributtes又是ConstraintDescription的属性,至于符号+=是作者对系统集合API的一个封装,见下面

internal func +=(left: inout ConstraintAttributes, right: ConstraintAttributes) {
    left.formUnion(right)
}

也即是将相应的布局约束属性放到self.description.attributes中, 因为在我们制作约束的时候需要用到Constraint类(上面已经讲过了),添加约束、激活约束、甚至移除约束都离不开改属性的存在 或者说离不开 结构体ConstraintAttributes的存在。

好,现在该穿越了,回到我们的最初的ConstraintMaker类中:

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

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

.......................................................等等

可以看到,我们经常使用的形式make.left.equalTo(还有很多)中的left就是一个ConstraintMakerExtendable的实例对象, 返回的是调用方法makeExtendableWithAttributes的结果,这个方法文章中已经写过,不再重复,不懂或忘记的回过头看看。如此,布局就“万事俱备只欠东风”了,最后一步就是equalTo,这也是必不可少的那么equalTo究竟又是什么呢?

接着往下看你就知道了.............................

ConstraintMakerEditable类的布局浅析

在这里,equalTo()ConstraintMakerRelatable类的实例方法返回的ConstraintMakerEditable类的实例,在文章开头的文件结构中我们已经知道,ConstraintMakerEditable继承于ConstraintMakerPriortizable , 而ConstraintMakerPriortizable 继承于ConstraintMakerFinalizable。 之前说过widthheightright等等其实就是ConstraintMakerExtendable类的实例,所以有make.width.equalTo()的形式,而ConstraintMakerExtendable类又继承于ConstraintMakerRelatable类。这样的话,我们来看一下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? 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
}

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

其中,other就是我们传入的目标值或对象,它是一个ConstraintRelatableTarget协议,比如我传入一个常量100, 约束就是:

make.width.equalTo(100)

我们来看一下ConstraintRelatableTarget协议里有什么:

public protocol ConstraintRelatableTarget {
}

extension Int: ConstraintRelatableTarget {
}

extension UInt: ConstraintRelatableTarget {
}

extension Float: ConstraintRelatableTarget {
}

extension Double: ConstraintRelatableTarget {
}

extension CGFloat: ConstraintRelatableTarget {
}

extension CGSize: ConstraintRelatableTarget {
}

extension CGPoint: ConstraintRelatableTarget {
}

extension ConstraintInsets: ConstraintRelatableTarget {
}

extension ConstraintItem: ConstraintRelatableTarget {
}

extension ConstraintView: ConstraintRelatableTarget {
}

@available(iOS 9.0, OSX 10.11, *)
extension ConstraintLayoutGuide: ConstraintRelatableTarget {
}

这样就非常明确了,为什么equalTo()里面既可以是某个特定的,也可以是一个对象,因为你可以传入IntDouble等类型的值,也可以传一个ConstraintView类型的实例对象,这样,所有的基本工作几乎都做好了。那么,你就可以通过目标视图对象、位移、位置关系等来确定两个视图的位置关系,从而制作符合需求的相应的约束。

SnapKit的应用建议

尽量采用简洁的写法,不要画蛇添足,尽管SnapKit布局很是灵活,但是你也不能随心所欲,毕竟,团队写的代码还是需要有一个统一标准的,至少,你写的布局,要让别人能够清晰可见。

下面简洁的布局写法

  • 宽和高相等,并且等于某个具体得值,比如100

建议:

make.size.equalTo(100)

不建议写成:

make.width.height.equalTo(100)

不建议写成

make.width.equalTo(100)
make.height.equalTo(100)
  • 宽和高不相等,并且等于某个具体值, 比如宽=100,高=200

建议写成

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

不建议写成:

make.width.equalTo(100)
make.height.equalTo(200)
  • 约束关系与某个视图相等

建议写成

make.left.equalTo(view2)
make.right.equalTo(view2)
make.top.equalTo(view2)
...............................................

如果同时有几个约束关系相等,建议用链式语法,增强可读性,比如

make.left.right.top.bottom.width.height.size.equalTo(view2)

不建议写成

make.left.equalTo(view.snp.left)
make.right.equalTo(view.snp.right)
make.top.equalTo(view.snp.top)
  • 如果等于父视图,直接使用equalToSuperview()

比如,view2是父视图,所要布局视图的顶部等于父视图的顶部

建议这样写

make.top.equalToSuperview()

不建议这样写:

make.top.equalTo(view2)
make.top.equalTo(view2.snp.top)
  • 距离父视图某个边缘多少距离 (同一性质)

前提:所布局视图的关系和父视图的关系是同一性质的,什么叫同一性质,比如top等于父视图的top,left等于父视图的left等等

假设view2为父视图, 距离父视图顶部100dp的距离

注:left、right、bottom等同下。

建议写成

make.top.equalTo(100)

不建议写成:

make.top.equalToSuperview().offset(100)
make.top.equalTo(view2).offset(100)
  • 距离父视图某个边缘多少距离 (不同性质)

如: left 距离父视图的right为 200

make.left.equalTo(view2.snp.right).offset(200)

SnapKit源码浅析总结

该库的源码逻辑清晰,封装得很好,善于将classstructprotoco结合起来使用,文件结构也很清晰,尽管分为这么多的文件,但是每个文件相互独立有相互依赖,都实现特定的功能,层级简单,如作者用四五个文件单独为特定的classprotocolenum定义别名。

另外就是代码十分简洁,每个文件你仔细去看,34个文件只有一个文件超过200行代码。

但是,个人觉得文件太多,对类、协议等的分离太多,对于新手来说,有时候很难弄清楚功能的实现,因为你研究一个功能,就会牵扯到很多很多文件,很多类,很多协议,可能一时会有点懵逼。但是好的地方就是,你研究清楚结构后,弄清楚每个文件的作用后,就会发现比较简单,这也是为何这么多文件,这么多类和协议,但是最终我们布局的时候,几行代码就搞定了布局。

就好比你寒窗苦读,努力工作, 成为了一个很有才华的人,你的一言一行别人都觉得是名言,时刻用来提醒自己。

但是,谁又知道,你惊人的才华的外表背后,都是你对多年的血与泪的努力的一个封装。

欢迎加入 iOS(swift)开发互助群:QQ群号: 558179558, 相互讨论和学习!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念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

推荐阅读更多精彩内容