iOS自动布局与VFL

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

自动布局的流程

iOS自动布局与VFL_第1张图片
image.png

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

iOS自动布局与VFL_第2张图片
image.png

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

LayoutEngine的属性

iOS自动布局与VFL_第3张图片
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布局:


iOS自动布局与VFL_第4张图片
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还是不通。


iOS自动布局与VFL_第5张图片
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
    }
多个子元素布局
iOS自动布局与VFL_第6张图片
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发生关系,不能做动画之外,基本可以完成大部分的布局功能,而且代码可读性其实还是很高的,所以强烈建议大家使用。

你可能感兴趣的:(iOS自动布局与VFL)