SnapKit源码解析

2022-10-14  本文已影响0人  Mr_Baymax

简介

什么是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)
}

注意事项

parentView.addSubview(subview)

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

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

使用教程

源码解析

详细解析

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

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

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

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

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

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 对象。

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对象。

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 这个类主要是设置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) 的引用

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

SnapKit 源码结构图

结构图.png

SnapKit 源码类图

类图.png
上一篇下一篇

猜你喜欢

热点阅读