iOS自动布局与VFL

作者: BurgerKing | 来源:发表于2018-05-14 22:45 被阅读112次

    最近在写UI的时候发现有些自动布局的东西忘记了,所以决定记录总结一下,而且发现VFL是一个很好用的东西,强烈推荐大家使用。

    自动布局的流程

    image.png

    这个图可以看出布局引擎是一个黑盒,我们的视图,属性,约束,本身宽高输入到引擎当中最后实现成我们的想要的样式。

    image.png

    这张图描述了布局的具体的三个流程,第一步是更新约束,第二步是更新layout,第三部是显示。这张图我们可以看出一个问题,layout的数据都来自自动布局的数据,如果混合使用手动布局和自动布局,可能会出现一些奇奇怪怪得问题,比如兄弟节点中设置了手动布局,其他兄弟节点的约束可能就可能会出错,因为设置了手动布局的兄弟节点找不到约束条件。

    LayoutEngine的属性

    image.png

    这张图展示了LayoutEngine的大部分属性,后续我们在自动布局当中需要设置我们需要的属性。

    自动布局重要参数

    1.translatesAutoresizingMaskIntoConstraints

    如果要使用自动布局,这个属性一定要设置为false,不转换AutosizeMask 到约束,经常由于调试了半天,发现一直不对,结果这个变量没有设置。
    官方文档介绍:
    https://developer.apple.com/documentation/uikit/uiview/1622572-translatesautoresizingmaskintoco

    2. intrinsicContentSize

    这个属性表示组件固有大小,常见的UILabel,UIImage,UIButton都能够根据内容自动填充大小,其他UI组件不是自动填充,所以如果我们需要用到其他UI组件可以重写这个属性,设置组件大小。

    3.contentHugging

    内容拥抱属性表示假如父容器有多余的空间,内容拥抱优先级越小的暂用的父容器的空间越大,即是否暂用父容器多余的空间。

    4.setContentCompression

    内容压缩属性表示当父容器没有多余的空间显示孩子节点,内容压缩阻力优先级越大的越抗压缩,内容压缩阻力优先级越小的被挤压的越厉害。

    举个🌰
    一个UITabviewCell 里面有两个UILabel,如果我们要让UILabel中间的间隔是10个point,上下对齐UITabviewCell,则我们的VFL可以这么写。

    self.contentView.addConstraints(NSLayoutConstraint.constraints(
                withVisualFormat: "H:|[l]-10-[r]|", options: [], metrics: nil,
                views: ["l":_left,"r":_rigint]))
    self.contentView.addConstraints(NSLayoutConstraint.constraints(
                withVisualFormat: "V:|[l]", options: [], metrics: nil,
                views: ["l":_left,]))
    

    H:|[l]-10-[r]|
    H 代表水平方向
    | 代表父view
    [l] 代表 mertics key-value 中对应的_left (UILabel)
    -10- 代表中间有10个点。
    [r] 代表 mertics key-value 中对应的_right (UILabel)
    水平方向就定义完了。

    定义完了水平方向还需要定义垂直方向

    距离父容器上边缘对齐10个点
    self.contentView.addConstraints(NSLayoutConstraint.constraints(
          withVisualFormat: "V:|-10-[l]", options: [], metrics: nil,
                views: ["l":_left,]))
    V:|[l] 只需要设置[l] 左边的竖线
    
    
    image.png
    
    距离父容器下边缘对齐10个点
    self.contentView.addConstraints(NSLayoutConstraint.constraints(
          withVisualFormat: "V:[l]-10-|", options: [], metrics: nil,
                views: ["l":_left,]))
    V:[l]| 只需要设置[l] 右边的竖线
    
    image.png
    
    距离父容器上下边缘对齐10个点
    self.contentView.addConstraints(NSLayoutConstraint.constraints(
          withVisualFormat: "V:|-10-[l]-10-|", options: [], metrics: nil,
                views: ["l":_left,]))
    V:[l]| 需要同时设置[l] 左右边的竖线,并且中间加入-10-
    
    image.png
      _left.setContentHuggingPriority(UILayoutPriority.defaultHigh, for: UILayoutConstraintAxis.horizontal)
    设置左边的ContentHugging 优先级为高,横轴
    
    image.png

    由于左边的ContentHugging 优先级比较高,在父容器有多余空间的情况下,ContentHugging优先级越低的暂用的空间越多。

            _left.text = "1234123412341234123412341234"
            _rigint.text = "abcdeabcdeabcdeabcdeabcdeabcde"
           _rigint.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
    

    当Label内容过多的时候,我们可以设置ContentCompressionResistance优先级来确定那个控件占用空间更多或者更少,现在我们设置右边的抗压缩优先级为低,所以右边的控件受到挤压。


    image.png

    我们经常在UITabviewCell中设置多行显示的Text的情况:



    左边的Label 显示两行,右边的Label显示一行,且左边的宽度==100 个点
    override func setText() {
    guard let _left = leftTitle,let _rigint = rightTitle else {
      return
     }
      _left.text = "1234567890123456789012345678901234567890"
      _left.numberOfLines = 2
      _rigint.text = "abcdefghijkabcdefghijkabcdefghijkabcdefghijk"
     }
    override func makeLayout() {
       guard let _left = leftTitle,let _rigint = rightTitle else {
      return
        }
      self.contentView.addConstraints(NSLayoutConstraint.constraints(
      withVisualFormat: "H:|[l(==100)]-10-[r]|", options: [.alignAllCenterY],     metrics: nil, views: ["l":_left,"r":_rigint]))
       self.contentView.addConstraints(NSLayoutConstraint.constraints(
      withVisualFormat: "V:|[l]|", options: [], metrics: nil,views: ["l":_left,]))
      self.contentView.addConstraints(NSLayoutConstraint.constraints(
       withVisualFormat: "V:|[r]|", options: [], metrics: nil,views: ["r":_rigint,]))
     }
    

    options: [.alignAllCenterY] 让所有的兄弟节点的centerY相同,左边的Label是两行,右边的Label是一行,都是centerY对齐。

    常见的UITableCell布局:


    image.png

    让图片居左,上下居中对齐,右边的标题距离上下10个点,下面多行排列。

      func makeLayout() {
            guard let _left = leftTitle,let _rigint = rightTitle, let _img = iconImage else {
                return
            }
            self.contentView.addConstraints(NSLayoutConstraint.constraints(
                withVisualFormat: "H:|-[i]-10-[l]-10-|", options: [],
                metrics: nil, views: ["i":_img,"l":_left]))
            
            self.contentView.addConstraints(NSLayoutConstraint.constraints(
                withVisualFormat: "H:|-[i]-10-[r]-10-|", options: [],
                metrics: nil, views: ["i":_img,"r":_rigint]))
            
            self.contentView.addConstraints(NSLayoutConstraint.constraints(
                withVisualFormat: "V:|-10-[l]-10-[r]-10-|", options: [],
                metrics: nil, views: ["l":_left,"r":_rigint]))
    
            cons_image_width = NSLayoutConstraint(item: _img, attribute: .width, relatedBy: .equal, toItem: nil, attribute: .notAnAttribute, multiplier: 1.0, constant: 40)
    
            cons_image_height = NSLayoutConstraint(item: _img, attribute: .height, relatedBy: .equal, toItem: nil, attribute: .notAnAttribute, multiplier: 1.0, constant: 40)
    
            self.contentView.addConstraints([ NSLayoutConstraint(item: _img, attribute: .centerY, relatedBy: .equal, toItem: self.contentView, attribute: .centerY, multiplier: 1.0, constant: 1.0), cons_image_width!,cons_image_height!] )
        }
    
    H:|-[i]-10-[l]-10-|
    H:|-[i]-10-[r]-10-|

    首先分别设置两个横向的方位,图片距离父容器15个点(-)默认是15个点,因为横向的方位有两个维度,所以分别对右边的两个UILabel设置了横向的VFL。

    V:|-10-[l]-10-[r]-10-|

    纵向的方位只设置了UILabel,Label之间是10个点,上下距离父容器10个点。

    UIImage 居中对齐。

    UIImage剧中对齐VFL这种表达方式不能实现,所以只好换一种方式(NSLayoutConstraint)去实现。因为VFL这种语言只能描述兄弟之间的关系,不能描述自己与父亲之间的关系。

    I hope in the future Apple adds some kind of new option to have the VFL options take into account the superview, even if doing it only when there is only a single explicit view besides the superview in the VFL.

    https://stackoverflow.com/questions/12873372/centering-a-view-in-its-superview-using-visual-format-language/14917695#14917695

    自动布局与动画

    VFL还有一个缺点就是实现动画很麻烦,因为一条VFL其实里面有很多个NSLayoutConstraint,而我们就不好去辨别要动用那个NSLayoutConstraint,所以一般情况如果我们的布局需要动画,最好用原始的NSLayoutConstraint去实现。

            var w_h:CGFloat = 0
            w_h = ani == true ? 100 :40
            self.contentView.layoutIfNeeded()
            UIView.animate(withDuration: 5) {
                self.cons_image_width?.constant = w_h
                self.cons_image_height?.constant = w_h
                self.contentView.layoutIfNeeded()
            }
    

    首先吧NSLayoutConstraint 保存起来,然后在 UIView.animate 中去改变 NSLayoutConstraint中的值来实现动画。

    UITableViewCell中的九宫格

    经常遇到这种需求,在UITableViewCell中嵌套一个CollectionView,这种情况感觉在自动布局里面坑是最多的。

    UITableView 自动布局:
     UITableView.translatesAutoresizingMaskIntoConstraints = false
     UITableView.estimatedRowHeight = 40
     UITableView.rowHeight = UITableViewAutomaticDimension
    
    UITableViewCell ContentView 上下顶住.
    self.contentView.addConstraints(
                NSLayoutConstraint.constraints(withVisualFormat: "H:|[collectionView]|",
                                               options: [],
                                               metrics: nil,
                                               views: ["collectionView": collectionView]))
    self.contentView.addConstraints(
                NSLayoutConstraint.constraints(withVisualFormat: "V:|[collectionView]|",
                                               options: [],
                                               metrics: nil,
                                               views: ["collectionView": collectionView]))
    

    如果要让UICollectionView 不滚动,网上有一种做法是直接设置UICollectionView 的 intrinsicContentSize 与 contentSize 相等,原理上这种方式可行。

    internal class NineGridCollectionView : UICollectionView {
        
        override func layoutSubviews() {
            super.layoutSubviews()
            if !self.bounds.size.equalTo(self.intrinsicContentSize) {
                self.invalidateIntrinsicContentSize()
            }
        }
        
        override var intrinsicContentSize: CGSize {
            print("contentSize = \(self.contentSize)")
            return self.contentSize
        }
    }
    

    但是现实情况却很复杂,在有些机器上可以,在有些机器上不行。
    UICollectionView; frame = (0 0; 345 204);contentSize: {345, 131};
    打印的内存数据现实contentSize 符合我们的预期,但是我们设置了与frame相同,最后现实的frame还是不通。


    123.png

    最后还是找了一种自动布局的方式去实现这种效果,现在看来应该没问题。通过计算设置.height 约束属性。

     contentView.addConstraints(
                NSLayoutConstraint.constraints(withVisualFormat: "H:|[collectionView(==self)]|",
                                               options: [],
                                               metrics: nil,
                                               views: ["collectionView": collectionView,
                                                       "self": contentView]))
     contentView.addConstraints(
                NSLayoutConstraint.constraints(withVisualFormat: "V:|[collectionView]|",
                                               options: [],
                                               metrics: nil,
                                               views: ["collectionView": collectionView])
            )
     heightConstraintOfCollectionView = NSLayoutConstraint(item: collectionView,
                                                                  attribute: .height,
                                                                  relatedBy: .equal,
                                                                  toItem: nil,
                                                                  attribute: .notAnAttribute,
                                                                  multiplier: 1.0,
                                                                  constant: 0.0)
     heightConstraintOfCollectionView?.isActive = true
    
      func updateCollectionViewHeightConstraint(height:Double) {
         heightConstraintOfCollectionView?.constant = height
        }
    
    多个子元素布局
    321.png

    这个UI组件一共有八个元素。
    1.最右边的箭头图标不能VFL去解决,只能用NSLayoutConstraint item 这种方式,因为VFL不能和父元素发生关系。
    2.最底部的一行有4个元素,刚开始我分别吧左右两个元素放入View当中,再吧这个View放入UITableView Cell 当中,但是这中间的价格可能小数点很多,换行之后高度就有变化,由于中间有一层View,所以导致不能直接给UITabview 高度压力,计算高度很麻烦,所以最后还是吧中间这层View去掉,而且退货必须再中间,所以最后只有计算宽度,写死 “进货” 与 “进货价格”的宽度(通过屏幕宽度动态计算)。
    进货与进货价格之间,进货的ContentHug的优先级高,进货价格的Contenhug优先级低,如果有空缺位置,需要由进货价格暂用,所以进货价格的内容压缩优先级低。

     let value_width = SCREEN_WIDTH * 0.5 -  t_w - CGFloat(left_padding) * 2 - CGFloat(hori_margin)
            self.contentView.addConstraints(
                NSLayoutConstraint.constraints(withVisualFormat:
                    "H:|-lp-[il]-hm-[iv(==vw)]-0-[rl]-hm-[rv(==vw)]-lp-|",
                                               options: [.alignAllFirstBaseline],
                                               metrics: ["lp": left_padding,
                                                         "hm": hori_margin,
                                                         "vw": value_width] ,
                                               views: [ "il": _importLabel,
                                                        "iv": _importValue,
                                                        "rl": _rejectLabel,
                                                        "rv": _rejectLabelValue]
                                                       ))
     _importLabel.setContentHuggingPriority(.defaultHigh, for: .horizontal)
     _importValue.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
    

    3.再设置列的时候有可能“退货的价格”比“进货的价格”高度要高一些,所以再设置列VFL的时候需要动态去计算那个元素的高一些,其实就是比较字符串长度长一些,这样避免整个Cell的高度不对。

    self.contentView.addConstraints( NSLayoutConstraint.constraints(withVisualFormat:
                    "V:|-ls-[tl]-ls-[ac]-ls-[il]-lls-[lv]|",
                                               options: [],
                                               metrics: ["ls": line_space,
                                                         "lls": line_space * 2] ,
                                               views: ["tl": _titleLabel,
                                                       "ac": _settleAcountValueLabel(结算值),
                                                       "il": importLengthMax == true ? _importValue(进货值) : _rejectLabelValue(退货值),
                                                       "lv": _lineView]))
            self.contentView.addConstraints(
    

    总结:
    VFL 还是很强大,除了不能和父View发生关系,不能做动画之外,基本可以完成大部分的布局功能,而且代码可读性其实还是很高的,所以强烈建议大家使用。

    相关文章

      网友评论

      • 舒马赫:受不了自动布局的冗长语法,推荐使用xml的布局库FlexLib,采用前端布局标准flexbox(不使用autolayout),支持热刷新,国际化等。可以到这里了解详细信息:

        https://github.com/zhenglibao/FlexLib

      本文标题:iOS自动布局与VFL

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