SnapKit浅析,原理

SnapKit的原理是啥,或者说是怎么实现自动布局的?

  1. SnapKit是Swift开发中常用的自动布局的三方库,虽然他让我们写UI布局方便了很多,但是他还是基于系统提供的API做的封装,也就是说,自动布局是系统提供的方法。系统提供的约束类是NSLayoutConstraint

  2. 咱们在用自动布局的时候,其实就是在写一个公式 view1.attr1 = view2.attr2 * multiplier + constan 或者 view1.attr1 <= view2.attr2 * multiplier + constantt 或者 view1.attr1 >= view2.attr2 * multiplier + constant,就是view1的某个边,或者宽高,等于(或者大于等于,或者小于等于)另一个view2的某一个边或者宽高,然后倍数是多少,偏移量是多少,比如

    view1.snp.makeConstraints { (make) in
          make.centerY.equalTo(view2)
          make.height.equalTo(view2)
          make.left.equalTo(view2).offset(12)
          make.width.equalTo(40)
     }
    
  3. 看看系统的API是啥样的呢,NSLayoutConstraint有两种写法,一种是用 VFL语言的规则,官方API如下

    /* Create an array of constraints using an ASCII-art-like visual format string.  The values of       the `metrics` dictionary should be NSNumber (or some other type that responds to -doubleValue and returns a double).  */
    @available(iOS 6.0, *)
    open class func constraints(withVisualFormat format: String, options opts: NSLayoutConstraint.FormatOptions = [], metrics: [String : Any]?, views: [String : Any]) -> [NSLayoutConstraint]
    
    

    写法呢就是咱们偶尔会见到的,如下

    V:|-0-[title]-0-|"
    

    语法规则就是这种

    • "H" 表示水平方向,"V"表示垂直方向;
    • "|" 表示superview的边界;
    • "[]" 表示view,"()"表示尺寸
    • "-" 表示间隙;
    • "@"表示优先级

    第一种不是重点,因为SnapKit不是基于这种写法实现的,今天既然讲的是SnapKit浅析,所以不会着重将第一种(其实是我不习惯这种写法,总是忘记每个符号代表啥)

    第二种的API如下

    /* Create constraints explicitly.  Constraints are of the form "view1.attr1 = view2.attr2 * multiplier + constant"
        If your equation does not have a second view and attribute, use nil and NSLayoutAttributeNotAnAttribute.
        Use of this method is not recommended. Constraints should be created using anchor objects on views and layout guides.
    */
       @available(iOS 6.0, *)
       public convenience init(item view1: Any, attribute attr1: NSLayoutConstraint.Attribute, relatedBy relation: NSLayoutConstraint.Relation, toItem view2: Any?, attribute attr2: NSLayoutConstraint.Attribute, multiplier: CGFloat, constant c: CGFloat)
    

    个人感觉第二种简单易懂,明确表示啥关系,不需要记住各种字符,只要遵循这个公式就可以:view1.attr1 = view2.attr2 * multiplier + constan 或者 view1.attr1 <= view2.attr2 * multiplier + constantt 或者 view1.attr1 >= view2.attr2 * multiplier + constant,SnapKit就是对第二种用法的封装,后面会详细说明

如果不用SnapKit,咱们怎么实现自动布局呢

  1. 如果不用SnapKit,咱们就直接用系统提供的API进行自动布局,让咱们试试系统API用法和效果咋样

接下来咱们就开始用第二种开发了,如第二种API中写的:如果view1的布局和view2的布局有关系,则就遵循view1.attr1 = view2.attr2 * multiplier + constant这个规则,但是如果两个没关系,比如view1的宽就是固定值100,是和view2没关系的,那就将view2赋值成nil,attr2赋值成NSLayoutAttributeNotAnAttribute

我创建一个UILabel,添加到view上,label的顶部距view的距离是100,label的左边距view的距离是30,label的宽是小于等于200,那么约束可以这样写

let lable = UILabel()
lable.backgroundColor = .red
lable.numberOfLines = 0
lable.text = "The constraint must involve only views that are within scope of the receiving view. Specifically, any views involved must be either the receiving view itself, or a subview of the receiving view."
self.view.addSubview(lable)
     
let topConstraint = NSLayoutConstraint(item: lable, attribute: NSLayoutConstraint.Attribute.top, relatedBy: NSLayoutConstraint.Relation.equal, toItem: self.view, attribute: NSLayoutConstraint.Attribute.top, multiplier: 1, constant: 100)
     
let leadingConstraint = NSLayoutConstraint(item: lable, attribute: NSLayoutConstraint.Attribute.left, relatedBy: NSLayoutConstraint.Relation.equal, toItem: self.view, attribute: NSLayoutConstraint.Attribute.leading, multiplier: 1, constant: 30)

//label的宽度和view没关系时,可以传nil和NSLayoutConstraint.Attribute.notAnAttribute
let widthConstraint = NSLayoutConstraint(item: lable, attribute: NSLayoutConstraint.Attribute.width, relatedBy: NSLayoutConstraint.Relation.lessThanOrEqual, toItem: nil, attribute: NSLayoutConstraint.Attribute.notAnAttribute, multiplier: 1, constant: 200)

然后怎么让这些约束生效呢,方法就是把这些约束添加到对应的视图上,添加到哪个视图上,哪个视图就是接受约束的视图,也就是接受视图,那么规则就是。假如一个约束涉及到两个view,view1和view2,如果view1是view2的父视图,那么接收视图就是view1,因为view1和约束中涉及到的视图的关系不是本身就是子视图。假如view1和view2层级不是父子关系,则接收约束的视图,就是他俩的公共视图,只有他俩的公共视图,才满足条件如果你接收约束的视图不满足这个条件会怎么样呢,很简单就是,还记得你忘记把被约束的view添加到父视图上,然后崩溃的场景了吗,如下:没添加到父视图,但是添加了约束(因为这会导致找不到满足条件的接收视图,或者接收视图不满足条件,就会导致崩溃)

let blueView = UIView()
     blueView.tag = 666;
     blueView.translatesAutoresizingMaskIntoConstraints = false
     blueView.backgroundColor = .blue
     //self.view.addSubview(blueView)
     blueView.snp.makeConstraints { (make) in
         make.leading.equalTo(100)
         make.top.equalTo(200)
         make.width.height.equalTo(200)
     }
image.png
  1. 按照官方的规则,满足条件的就是self.view,所以就有了将约束添加到self.view上
self.view.addConstraint(topConstraint)
self.view.addConstraint(leadingConstraint)
self.view.addConstraint(widthConstraint)

然后就生效了吗,答案是没有,因为默认情况下一个view创建后,会根据当前的frame自动创建NSLayoutConstraint,所以再一次添加约束,就可能会导致约束冲突,所以需要让被约束的view不自动创建约束,所以还需要设置一下translatesAutoresizingMaskIntoConstraints

//If you want to use Auto Layout to dynamically calculate the size and position of your view, you must set this property to false

lable.translatesAutoresizingMaskIntoConstraints = false

这样就大功告成了,完整的代码如下

let lable = UILabel()
lable.translatesAutoresizingMaskIntoConstraints = false
lable.backgroundColor = .red
lable.numberOfLines = 0
lable.text = "The constraint must involve only views that are within scope of the receiving view. Specifically, any views involved must be either the receiving view itself, or a subview of the receiving view."
self.view.addSubview(lable)
     
let topConstraint = NSLayoutConstraint(item: lable, attribute: NSLayoutConstraint.Attribute.top, relatedBy: NSLayoutConstraint.Relation.equal, toItem: self.view, attribute: NSLayoutConstraint.Attribute.top, multiplier: 1, constant: 100)
     
let leadingConstraint = NSLayoutConstraint(item: lable, attribute: NSLayoutConstraint.Attribute.left, relatedBy: NSLayoutConstraint.Relation.equal, toItem: self.view, attribute: NSLayoutConstraint.Attribute.left, multiplier: 1, constant: 30)
     
let widthConstraint = NSLayoutConstraint(item: lable, attribute: NSLayoutConstraint.Attribute.width, relatedBy: NSLayoutConstraint.Relation.lessThanOrEqual, toItem: nil, attribute: NSLayoutConstraint.Attribute.notAnAttribute, multiplier: 1, constant: 200)
     
self.view.addConstraint(topConstraint)
self.view.addConstraint(leadingConstraint)
self.view.addConstraint(widthConstraint)
label.png

从iOS8以后,添加约束的方法变的很方便了,可以直接将约束的isActive设置为True,但是实质上还是上面说的。说addConstraint的方式就是为了说明约束添加的规则,以及为啥不添加父视图上就会崩溃

topConstraint.isActive = true
leadingConstraint.isActive = true
widthConstraint.isActive = true

NSLayoutConstraint还有哪些用法

  1. bottom、lastBaseline、bottomMargin有啥区别
    下图创建了黄、蓝、绿三个View,大小是一样的,区别就在于他们的顶部约束是不一样的,
    黄色视图的top等于label的bottom,蓝色视图的top等于label的lastBaseline,绿色视图等于label的bottomMargin,代码如下

    let yellowViewTopConstraint = NSLayoutConstraint(item: yellowView, attribute: NSLayoutConstraint.Attribute.top, relatedBy: NSLayoutConstraint.Relation.equal, toItem: label, attribute: NSLayoutConstraint.Attribute.bottom, multiplier: 1, constant: 0)
    
    let blueViewTopConstraint = NSLayoutConstraint(item: blueView, attribute: NSLayoutConstraint.Attribute.top, relatedBy: NSLayoutConstraint.Relation.equal, toItem: label, attribute: NSLayoutConstraint.Attribute.lastBaseline, multiplier: 1, constant: 0)
    
    let greenViewTopConstraint = NSLayoutConstraint(item: greenView, attribute: NSLayoutConstraint.Attribute.top, relatedBy: NSLayoutConstraint.Relation.equal, toItem: label, attribute: NSLayoutConstraint.Attribute.bottomMargin, multiplier: 1, constant: 0)
    

    效果如下

    黄、蓝、绿三个View.png

    结论:left、right、top、bottom都是代表视图的边界
    firstBaseline 、lastBaseline 代表Label或Button上文字顶部和文字底部的边界,并不是label或Button的边界
    bottomMargin代表视图边缘空白处,大部分视图默认是8,但是也会根据安全区域变化,想了解更深的可以自己找资料看下

  2. 约束优先级
    比如布局了黄蓝绿三个View,蓝色View的left距离黄色View右边20,绿色view的左边距离蓝色View的右边20,布局如下


    约束优先级.png

    用原生的NSLayoutConstraint写法是

    let yellowView = UIView()
    yellowView.translatesAutoresizingMaskIntoConstraints = false
    yellowView.backgroundColor = .yellow
    self.view.addSubview(yellowView)
         
    let yellowViewTopConstraint = NSLayoutConstraint(item: yellowView, attribute: NSLayoutConstraint.Attribute.top, relatedBy: NSLayoutConstraint.Relation.equal, toItem: self.view, attribute: NSLayoutConstraint.Attribute.top, multiplier: 1, constant: 100)
         
    let yellowViewLeadingConstraint = NSLayoutConstraint(item: yellowView, attribute: NSLayoutConstraint.Attribute.left, relatedBy: NSLayoutConstraint.Relation.equal, toItem: self.view, attribute: NSLayoutConstraint.Attribute.left, multiplier: 1, constant: 30)
         
    let yellowViewWidthConstraint = NSLayoutConstraint(item: yellowView, attribute: NSLayoutConstraint.Attribute.width, relatedBy: NSLayoutConstraint.Relation.equal, toItem: nil, attribute: NSLayoutConstraint.Attribute.notAnAttribute, multiplier: 1, constant: 100)
         
    let yellowViewHeightConstraint = NSLayoutConstraint(item: yellowView, attribute: NSLayoutConstraint.Attribute.height, relatedBy: NSLayoutConstraint.Relation.equal, toItem: nil, attribute: NSLayoutConstraint.Attribute.notAnAttribute, multiplier: 1, constant: 100)
         
    yellowViewTopConstraint.isActive = true
    yellowViewLeadingConstraint.isActive = true
    yellowViewWidthConstraint.isActive = true
    yellowViewHeightConstraint.isActive = true
         
    let blueView = UIView()
    blueView.tag = 666;
    blueView.translatesAutoresizingMaskIntoConstraints = false
    blueView.backgroundColor = .blue
    self.view.addSubview(blueView)
         
    let blueViewTopConstraint = NSLayoutConstraint(item: blueView, attribute: NSLayoutConstraint.Attribute.top, relatedBy: NSLayoutConstraint.Relation.equal, toItem: yellowView, attribute: NSLayoutConstraint.Attribute.top, multiplier: 1, constant: 0)
         
    let blueViewLeadingConstraint = NSLayoutConstraint(item: blueView, attribute: NSLayoutConstraint.Attribute.left, relatedBy: NSLayoutConstraint.Relation.equal, toItem: yellowView, attribute: NSLayoutConstraint.Attribute.right, multiplier: 1, constant: 20)
         
    let blueViewWidthConstraint = NSLayoutConstraint(item: blueView, attribute: NSLayoutConstraint.Attribute.width, relatedBy: NSLayoutConstraint.Relation.equal, toItem: nil, attribute: NSLayoutConstraint.Attribute.notAnAttribute, multiplier: 1, constant: 100)
         
    let blueViewHeightConstraint = NSLayoutConstraint(item: blueView, attribute: NSLayoutConstraint.Attribute.height, relatedBy: NSLayoutConstraint.Relation.equal, toItem: nil, attribute: NSLayoutConstraint.Attribute.notAnAttribute, multiplier: 1, constant: 100)
         
    blueViewTopConstraint.isActive = true
    blueViewLeadingConstraint.isActive = true
    blueViewWidthConstraint.isActive = true
    blueViewHeightConstraint.isActive = true
         
    let greenView = UIView()
    greenView.translatesAutoresizingMaskIntoConstraints = false
    greenView.backgroundColor = .green
    self.view.addSubview(greenView)
         
    let greenViewTopConstraint = NSLayoutConstraint(item: greenView, attribute: NSLayoutConstraint.Attribute.top, relatedBy: NSLayoutConstraint.Relation.equal, toItem: blueView, attribute: NSLayoutConstraint.Attribute.top, multiplier: 1, constant: 0)
         
    let greenViewLeadingConstraint = NSLayoutConstraint(item: greenView, attribute: NSLayoutConstraint.Attribute.left, relatedBy: NSLayoutConstraint.Relation.equal, toItem: blueView, attribute: NSLayoutConstraint.Attribute.right, multiplier: 1, constant: 20)
         
    let greenViewWidthConstraint = NSLayoutConstraint(item: greenView, attribute: NSLayoutConstraint.Attribute.width, relatedBy: NSLayoutConstraint.Relation.equal, toItem: nil, attribute: NSLayoutConstraint.Attribute.notAnAttribute, multiplier: 1, constant: 100)
         
    let greenViewHeightConstraint = NSLayoutConstraint(item: greenView, attribute: NSLayoutConstraint.Attribute.height, relatedBy: NSLayoutConstraint.Relation.equal, toItem: nil, attribute: NSLayoutConstraint.Attribute.notAnAttribute, multiplier: 1, constant: 100)
         
    greenViewTopConstraint.isActive = true
    greenViewLeadingConstraint.isActive = true
    greenViewWidthConstraint.isActive = true
    greenViewHeightConstraint.isActive = true
         
    let button = UIButton(frame: CGRect(x: 100, y: 400, width: 200, height: 50))
    button.setTitle("删除蓝色视图", for: UIControl.State.normal)
    button.backgroundColor = .red
    button.setTitleColor(.black, for: UIControl.State.highlighted)
    self.view.addSubview(button)
    button.addTarget(self, action: #selector(test3Action), for: UIControl.Event.touchUpInside)
    
    • 假如这个时候我想点击按钮删除蓝色View,那么在点击事件里应该怎么做?常规做法是:首先将蓝色View移除,然后重写绿色View的约束。但是如果利用约束优先级的属性,就只需要将蓝的View移除就可以了

    • 说到优先级咱们先说一下约束冲突,比如你给一个View添加了一个宽度为100的约束,然后又添加了一个宽度为200的约束,这个时候系统就不知道以哪个为准了,然后就会报一个咱们常见的错误

      Constraint[41416:11124704] [LayoutConstraints] Unable to simultaneously satisfy constraints.
       Probably at least one of the constraints in the following list is one you don't want. 
       Try this: 
           (1) look at each constraint and try to figure out which you don't expect; 
           (2) find the code that added the unwanted constraint or constraints and fix it. 
      

      但是每个约束都是支持设置优先级的,文档如下,优先级是1-1000,默认值是1000

       /* If a constraint's priority level is less than required, then it is optional.  Higher priority constraints are met before lower priority constraints.
      Constraint satisfaction is not all or nothing.  If a constraint 'a == b' is optional, that means we will attempt to minimize 'abs(a-b)'.
      This property may only be modified as part of initial set up or when optional.  After a constraint has been added to a view, an exception will be thrown if the priority is changed from/to NSLayoutPriorityRequired.
      */
        open var priority: UILayoutPriority
      

      假如你第一个添加的宽度为100的约束时高优先级,第二个宽度为200的约束时低优先级,这时系统发现两个约束有冲突时,就会优先以高优先级的约束为准,当高优先级的约束被移除后,低优先级的约束会生效

    • 基于这种特性,咱们可以给绿色View的left添加两个约束,第一个约束时距离蓝色View右边20,优先级是1000,第二个约束时距离黄色View右边20,优先级是750,这时绿色View优先是在蓝色View后面,当蓝色View被移除时,备用约束生效,绿色View会自动跟在黄色View后面。在之前代码基础上添加备用约束

      let greenViewTopConstraint2 = NSLayoutConstraint(item: greenView, attribute: NSLayoutConstraint.Attribute.top, relatedBy: NSLayoutConstraint.Relation.equal, toItem: yellowView, attribute: NSLayoutConstraint.Attribute.top, multiplier: 1, constant: 0)
      greenViewTopConstraint2.priority = UILayoutPriority(rawValue: 750)
      
      let greenViewLeadingConstraint2 = NSLayoutConstraint(item: greenView, attribute: NSLayoutConstraint.Attribute.left, relatedBy: NSLayoutConstraint.Relation.equal, toItem: yellowView, attribute: NSLayoutConstraint.Attribute.right, multiplier: 1, constant: 20)
      greenViewLeadingConstraint2.priority = UILayoutPriority(rawValue: 750)
      
      greenViewTopConstraint2.isActive = true
      greenViewLeadingConstraint2.isActive = true 
      
       @objc func test3Action() {
           let blueView = self.view.viewWithTag(666)
           blueView?.removeFromSuperview()
       }
      

      如下面GIF点击按钮,移除蓝色View后,绿色View会自动跟上

删除一个View.gif
  1. 假如cell上横向布局两个label,一个是标题,一个是内容,不能换行,如果文案过长,导致不能将标题和内容全部显示全的情况下,怎么保证标题显示全,或者内容显示全,如图


    两个Label并列.png

    普通布局的代码如下

    let titleLabel = UILabel()
    titleLabel.translatesAutoresizingMaskIntoConstraints = false
    titleLabel.backgroundColor = .red
    titleLabel.text = "我是左边标题,我很长很长,我显示全了"
    self.view.addSubview(titleLabel)
    
    let topConstraint = NSLayoutConstraint(item: titleLabel, attribute: NSLayoutConstraint.Attribute.top, relatedBy: NSLayoutConstraint.Relation.equal, toItem: self.view, attribute: NSLayoutConstraint.Attribute.top, multiplier: 1, constant: 150)
    
    let leadingConstraint = NSLayoutConstraint(item: titleLabel, attribute: NSLayoutConstraint.Attribute.left, relatedBy: NSLayoutConstraint.Relation.equal, toItem: self.view, attribute: NSLayoutConstraint.Attribute.left, multiplier: 1, constant: 10)
    
    topConstraint.isActive = true
    leadingConstraint.isActive = true
    
    let contentLabel = UILabel()
    contentLabel.translatesAutoresizingMaskIntoConstraints = false
    contentLabel.backgroundColor = .red
    contentLabel.text = "右边的内容,我显示全了"
    self.view.addSubview(contentLabel)
    
    let contentTopConstraint = NSLayoutConstraint(item: contentLabel, attribute: NSLayoutConstraint.Attribute.top, relatedBy: NSLayoutConstraint.Relation.equal, toItem: self.view, attribute: NSLayoutConstraint.Attribute.top, multiplier: 1, constant: 150)
    
    let contentLeadingConstraint = NSLayoutConstraint(item: contentLabel, attribute: NSLayoutConstraint.Attribute.left, relatedBy: NSLayoutConstraint.Relation.equal, toItem: titleLabel, attribute: NSLayoutConstraint.Attribute.right, multiplier: 1, constant: 10)
    
    let contenttrailingConstraint = NSLayoutConstraint(item: contentLabel, attribute: NSLayoutConstraint.Attribute.right, relatedBy: NSLayoutConstraint.Relation.equal, toItem: self.view, attribute: NSLayoutConstraint.Attribute.right, multiplier: 1, constant: -10)
    
    
    
    contentTopConstraint.isActive = true
    contentLeadingConstraint.isActive = true
    contenttrailingConstraint.isActive = true
    
    

咱们知道,在不设置label的宽高只设置原点的情况下,label也会把自己撑起来显示出文字,但是UIView就不行。原因在于Lable自带一个把自己撑起来的属性。当两个Label遇到一起,文案都很长,如上图所示,那哪个显示全,是不确定的,可能和添加顺序有关等等,怎么才能按照咱们的想法来呢,答案还是 优先级
Label自带把自己撑起来的属性,这个撑的能力是可以设置强弱的,这就需要熟悉下面两个方法

open func contentHuggingPriority(for axis: NSLayoutConstraint.Axis) -> UILayoutPriority
open func setContentCompressionResistancePriority(_ priority: UILayoutPriority, for axis: NSLayoutConstraint.Axis)

第一个方法contentHuggingPriority,这个可以设置Lable的拒绝变为大于其固有大小的优先级。
第二个方法setContentCompressionResistancePriority这个可以设置Label拒绝小于其固有大小的优先级。
他们默认的优先级是UILayoutPriorityDefaultLow 或者 UILayoutPriorityDefaultHigh,反正不是最高,有了这个属性,那就可以随意操作,想让哪个Label显示全,就让哪个显示全

假如咱们想让contentLabel显示全,则加上

contentLabel.setContentCompressionResistancePriority(UILayoutPriority.required, for: NSLayoutConstraint.Axis.horizontal)

结果肯定是


content显示全了.png

假如咱们想让titleLabel显示全,则加上

titleLabel.setContentCompressionResistancePriority(UILayoutPriority.required, for: NSLayoutConstraint.Axis.horizontal)

结果就是
title显示全了.png

left right 和 leading trailing有啥区别

  1. 全世界所有的人对于左和右都是统一的,也就是说,假如让全世界的人举起左手,那么全世界的人举起的都是同一根胳膊,咱们大部分国家的阅读习惯也是从做到右,所以咱们设计的UI,一般都是从左向右布局,宁愿右边空着,也不会左边空着,例如:
    习惯从左向又阅读.jpg
  1. 但是有特殊情况的国家,比如阿拉伯,假如你的APP不仅给中国人使用,想给全球人使用,包含阿拉伯等从右往左阅读习惯的人,那么,当你用left布局时,就违背了阿拉伯人的阅读习惯,就好比微信布局从右往左,会让人别扭。但是使用leading、trailing就不会出现,leading、trailing代表前后,可以理解为阅读的顺序,系统会根据当前手机的语言,将leading、trailing转换成left或者right。用中文,系统就把leading转换成left,用阿拉伯语就把leading转换成Right
Simulator Screen Shot - iPhone 12 - 2021-06-01 at 23.55.32.png
  1. 所以国际化的APP,用自动布局的时候,最好用leading、trailing代替left right

回到SnapKit

  1. 前面说了那么多NSLayoutConstraint,是因为SnapKit是基于NSLayoutConstraint封装的,想了解自动布局,必须得熟悉NSLayoutConstraint

  2. 为啥我用SnapKit时没写过translatesAutoresizingMaskIntoConstraints = false
    因为用SnapKit添加约束的时候,SnapKit会自动给咱们设置,源码如下

    internal func prepare() {
         if let view = self as? ConstraintView {
             view.translatesAutoresizingMaskIntoConstraints = false
         }
     }
    
  1. 用SnapKit添加约束时用makeConstraints,那updateConstraints怎么用
    • 从源码可以看出,如果之前没有添加过约束,也就是item.constraints.count <= 0, 那实际调用的就是makeConstraints,所以给视图第一次添加约束时,makeConstraints和updateConstraints是一样的
    internal static func updateConstraints(item: LayoutConstraintItem, closure: (_ make: ConstraintMaker) -> Void) {
            guard item.constraints.count > 0 else {
                self.makeConstraints(item: item, closure: closure)
                return
         }
         
         let constraints = prepareConstraints(item: item, closure: closure)
         for constraint in constraints {
             constraint.activateIfNeeded(updatingExisting: true)
         }
     }
    
    • 如果之前已经添加过约束了,那啥时候用updateConstraints?,咱们再回到NSLayoutConstraint官方API上说的,也就是说只有偏移量是可以更新的

      /* Unlike the other properties, the constant may be modified after constraint creation.  Setting the constant on an existing constraint performs much better than removing the constraint and adding a new one that's just like the old but for having a new constant.
      */
       open var constant: CGFloat
      

      也就是说下面的代码是没问题的,因为只更新了偏移量

       //这样更新时没问题的
       blueView.snp.makeConstraints { (make) in
             make.leading.equalTo(100)
             make.top.equalTo(200)
        }
      
       blueView.snp.updateConstraints { (make) in
             make.leading.equalTo(200)
         }
      

      但是假如我更新的不是偏移量,是view1.attr1 = view2.attr2 * multiplier + constan里面的view或者attr或者multiplier都会报错,如下代码

       //这样会报错
      blueView.snp.makeConstraints { (make) in
           make.leading.equalTo(self.view.snp.leading)
           make.top.equalTo(200)
       }
      
      blueView.snp.updateConstraints { (make) in
           make.leading.equalTo(self.view.snp.trailing)
      }
      

      就会出现下面错误

image.png
  • SnapKit会判断你调用updateConstraints时,更新的是不是偏移量constant,如果不是就会主动报错,那他是怎么判断的呢
    //首先判断你调用updateConstraints时判断更新的约束之前存不存在,如果不存在就报错,存在就更新成新的值,存不存在通过(updateLayoutConstraint = existingLayoutConstraint)判断
    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)")
                 }
    
    
    //怎么判断存不存在,判断的条件就是
     internal func ==(lhs: LayoutConstraint, rhs: LayoutConstraint) -> Bool {
     // If firstItem or secondItem on either constraint has a dangling pointer
     // this comparison can cause a crash. The solution for this is to ensure
     // your layout code hold strong references to things like Views, LayoutGuides
     // and LayoutAnchors as SnapKit will not keep strong references to any of these.
     guard lhs.firstAttribute == rhs.firstAttribute &&
           lhs.secondAttribute == rhs.secondAttribute &&
           lhs.relation == rhs.relation &&
           lhs.priority == rhs.priority &&
           lhs.multiplier == rhs.multiplier &&
           lhs.secondItem === rhs.secondItem &&
           lhs.firstItem === rhs.firstItem else {
         return false
     }
     return true
    }
    
    所以在已添加约束后调用updateConstraints时,里面写的约束要保证之前已经添加过对应的只有偏移量不一样的约束,否则就会报错\
  1. 如果我多次调用makeConstraints,添加约束会怎样

    • makeConstraints是不会检查之前是否已存在相同的约束,只要调用就会添加,所以如果添加了多次相同的约束,那么都会被添加上,只是因为约束一样不会报约束冲突
    • 但是如果多次添加的约束不一样就会有约束冲突。但是约束冲突不会崩溃,只是可能出现布局错乱和你想要的结果不一样
  2. 如果我多次调用makeConstraints添加相同的约束,然后再用updateConstraints更新约束,会将之前存在的相同约束都更新吗?
    答案是不会,只会更新一个已存在的约束,因为源码是如下这样写的, layoutConstraints是updateConstraints方法添加的约束,existingLayoutConstraints是之前makeConstraints的所有约束,外面的循环是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)")
                 }
    

    所以当报约束冲突时,有可能确实是写的约束有问题,也有可能是多次添加了不一样的约束,也有可能是多次添加了一样的约束,但只更新了一个约束

  3. lessThanOrEqualTo 、greaterThanOrEqualTo
    平时用的比较多的是equalTo,但是lessThanOrEqualTo、greaterThanOrEqualTo也是很有用的,再以前面刚说过的title和content两个Label举例,假如要求title最宽是200,当文字宽度小于200时把余下的空间给content,那就可以写成类似如下

    titleLabel.snp.makeConstraints { (maker) in
       maker.leading.equalToSuperview()
       maker.top.equalToSuperview()
       maker.width.lessThanOrEqualTo(200)
     }
    
    contentLabel.snp.makeConstraints { (maker) in
        maker.top.equalTo(titleLabel)
        maker.leading.equalTo(titleLabel.snp.trailing)
     }
    

你可能感兴趣的:(SnapKit浅析,原理)