最近在写UI的时候发现有些自动布局的东西忘记了,所以决定记录总结一下,而且发现VFL是一个很好用的东西,强烈推荐大家使用。
自动布局的流程
这个图可以看出布局引擎是一个黑盒,我们的视图,属性,约束,本身宽高输入到引擎当中最后实现成我们的想要的样式。
这张图描述了布局的具体的三个流程,第一步是更新约束,第二步是更新layout,第三部是显示。这张图我们可以看出一个问题,layout的数据都来自自动布局的数据,如果混合使用手动布局和自动布局,可能会出现一些奇奇怪怪得问题,比如兄弟节点中设置了手动布局,其他兄弟节点的约束可能就可能会出错,因为设置了手动布局的兄弟节点找不到约束条件。
LayoutEngine的属性
这张图展示了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] 左边的竖线
距离父容器下边缘对齐10个点
self.contentView.addConstraints(NSLayoutConstraint.constraints(
withVisualFormat: "V:[l]-10-|", options: [], metrics: nil,
views: ["l":_left,]))
V:[l]| 只需要设置[l] 右边的竖线
距离父容器上下边缘对齐10个点
self.contentView.addConstraints(NSLayoutConstraint.constraints(
withVisualFormat: "V:|-10-[l]-10-|", options: [], metrics: nil,
views: ["l":_left,]))
V:[l]| 需要同时设置[l] 左右边的竖线,并且中间加入-10-
_left.setContentHuggingPriority(UILayoutPriority.defaultHigh, for: UILayoutConstraintAxis.horizontal)
设置左边的ContentHugging 优先级为高,横轴
由于左边的ContentHugging 优先级比较高,在父容器有多余空间的情况下,ContentHugging优先级越低的暂用的空间越多。
_left.text = "1234123412341234123412341234"
_rigint.text = "abcdeabcdeabcdeabcdeabcdeabcde"
_rigint.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
当Label内容过多的时候,我们可以设置ContentCompressionResistance优先级来确定那个控件占用空间更多或者更少,现在我们设置右边的抗压缩优先级为低,所以右边的控件受到挤压。
我们经常在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布局:
让图片居左,上下居中对齐,右边的标题距离上下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.
自动布局与动画
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还是不通。
最后还是找了一种自动布局的方式去实现这种效果,现在看来应该没问题。通过计算设置.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
}
多个子元素布局
这个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发生关系,不能做动画之外,基本可以完成大部分的布局功能,而且代码可读性其实还是很高的,所以强烈建议大家使用。