美文网首页
SnapKit源码解析

SnapKit源码解析

作者: Mr_Baymax | 来源:发表于2022-10-14 11:00 被阅读0次

    简介

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

    相关文章

      网友评论

          本文标题:SnapKit源码解析

          本文链接:https://www.haomeiwen.com/subject/hsnqzrtx.html