iOS自动布局与VFL

最近在写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发生关系,不能做动画之外,基本可以完成大部分的布局功能,而且代码可读性其实还是很高的,所以强烈建议大家使用。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容