iOS自定义View 控件自动计算size能力
背景
在使用 UILabel 和 UIImage 的时候,不用指定宽高约束,控件也不会报约束缺失,还可以根据内容自己确定适合的宽高,特别适合 Xib 和 Storyboard 布局。
我们在自定义控件的时候,怎么才能让控件具备这种自动计算宽高的功能了?
基本知识点
只需要自定义控件满足以下条件之一即可:
自定义控件实现intrinsicContentSize
的重写并返回合适大小,适合子控件存在frame
初始化,或者动态添加。自定义控件内部约束满足
这时候有部分长得帅的小伙伴要问了,如果自定义控件即满足了所有约束,又重写了intrinsicContentSize
返回了计算大小怎么办。答案只有一个,执行约束,忽略 这时候有部分长得帅的小伙伴要问了,如果自定义控件即满足了所有约束,又重写了intrinsicContentSize
返回了计算大小怎么办。答案只有一个,执行约束,忽略intrinsicContentSize
.
intrinsicContentSize
intrinsicContentSize
又叫内容大小,决定了控件自动布局下的大小,(当控件没有宽高约束的时候,系统会自动添加宽高约束)自定义控件重写 intrisicContentsize 返回内在大小,自动布局系统就可以自己确定控件大小,就像使用UILabel,根据文本多少弹性控制大小。
自定义控件如果要适配AutoLayout, 达到自动适应的效果,则需要重写此属性。
override var intrinsicContentSize: CGSize {// 计算内容大小, 如果某个维度不使用内在大小可以设置为 UIView.noIntrinsicMetric//return CGSize(width: bounds.width, height: itemHeight*CGFloat(items.count))//返回计算的size}
UIView.noIntrinsicMetric
内在大小需要返回 CGSize, 就是你希望控件的内在大小。
但是可能存在,我的控件只想指定高度,宽度随外部约束。这时候就需要使用UIView.noIntrinsicMetric
使用-1
也可以,同样的效果。
比如 UIView 的内在大小默认值等于(UIView.noIntrinsicMetric,UIView.noIntrinsicMetric)也就是(-1,-1),表示宽高由外部控件外部约束决定,或者有创建的自动约束决定。
一旦重写并返回非noIntrinsicMetric, 那么控件自己就会为自己添加对应的约束。
比如我重写
override var intrinsicContentSize: CGSize {return CGSize(width: UIView.noIntrinsicMetric, height: itemH)}
打印约束自动添加
<NSContentSizeLayoutConstraint:0x600003650960 AnyTest.SequenceView:0x7fc91bf0a1b0.height == 162.5 Hug:250 CompressionResistance:750 (active)>
invalidateIntrinsicContentSize()
该方法的作用是重新计算intrinsicContentSize
,相当于原来的内置大小失效,重新生效一个新的。
func invalidateIntrinsicContentSize()
通常需要内在大小改变时候调用,如外部数据变化时。
约束调整周期
一般情况调用顺序
invlidateIntricContentSize() --> intrisicContenSize --> updateConstraints 之后会调用 layoutSubviews()。
如 UILabel 调整周期如下:
虚线表示可能调用, 可能某些步骤存在重复调用。
我们需要在合适的时机去调用invalidateIntrinsicContentSize()
方法以返回正确的内在大小。
比如当某个属性改变可能引起控件的内在大小改变时调用该方法,比如UILabel设置文本的时候应该会调用这个方法(猜测)。
但要注意跳出循环(可能会引起循环)。
override func layoutSubviews() {super.layoutSubviews()//if intrinsicContentSize() != bound.size {//计算相应的内置大小invalidateIntrinsicContentSize() // 重置内容大小。//}}
sizeToFit()
调整子视图大小和位置(不能重写),可以在外部或者内部调用,如果使内部调用,可以使得控件本身基于外部约束获取大小值(
func sizeToFit()
计算和返回视图合适的尺寸
/// size参数: 给定大小func sizeThatFits(_ size: CGSize) -> CGSize
自定义View内部宽度,可以在layousubViews中调用super.layoutSubviews后获得准确宽度。
override func layoutSubviews() {super.layoutSubviews()print(frame) // 这里获取的宽度是准备的。}
sizeTofit简单使用与理解(laughing–简书)
实例代码
@objc private func tap(){//此处引起内在大小改变为点击事件。self.invalidateIntrinsicContentSize() //先使内置大小无效,自动调用重写内置大小。// 改变的数据extentSize = CGSize(width: 0, height: 20)}// 重写改变内置大小override var intrinsicContentSize: CGSize{// 返回新大小return CGSize(width: bounds.width, height: bounds.height+extentSize.height)}
使用实例:只有20%的iOS程序员能看懂(自定义控件的随内容变换而自适应大小)
返回最佳的大小,基于当前的约束
// targetSize: layoutFittingCompressedSize(尽可能小) layoutFittingExpandedSize(尽可能大)func systemLayoutSizeFitting(_ targetSize: CGSize) -> CGSize
/// targetSize: 目标大小 如果需要尽可能小 使用layoutFittingCompressedSize, layoutFittingCompressedSize(尽可能大)/// horizontalFittingPriority: 水平约束的优先级。指定fittingSizeLevel以获得尽可能接近targetSize宽度值的宽度/// verticalFittingPriority: 垂直约束的优先级。指定fittingSizeLevel以获得尽可能接近targetSize高度值的高度func systemLayoutSizeFitting(_ targetSize: CGSize, withHorizontalFittingPriority horizontalFittingPriority: UILayoutPriority, verticalFittingPriority: UILayoutPriority) -> CGSize
参考:
github某issues 关于AutoLayout的讨论 推荐
Apple Autolayout相关文档
疑问
UILabel 的内部怎么计算出内在大小的?即在intrinsicContentSize属性返回值是怎么计算的。按照打印日志来看,是经过多次(超过四次)updateLayout, intrinsicContenSize调用得出的。
自定义视图实现了 intrinsicContentSize,如何适配在UITableViewCell中自适应行高?
经过测试,自定义视图实现intrinsicContentSize,会使用第一次返回的内置大小,如果是通过多次invalidateIntrinsicContentSize来确定内置大小,在UITableViewCell中使用将不会达到想要的效果。
参考
Intrinsic Content Size of Multi-Line Text
iOS 中编码约束
实例
实现如下自定义控件,外部传入黄色 view 数量,等比例计算黄色高度,计算intrinsicContentSize
示例代码
override var intrinsicContentSize: CGSize {return CGSize(width: UIView.noIntrinsicMetric, height: itemH)}
layoutSubviews
override func layoutSubviews() {super.layoutSubviews()updateItemsLayout()invalidateIntrinsicContentSize()}
完整代码
import UIKit@IBDesignableclass SequenceView: UIView {@IBInspectablevar count: Int = 0 {didSet {updateItemsCount()}}private var items: [UIView] = []private var itemH: CGFloat = 0override var intrinsicContentSize: CGSize {return CGSize(width: UIView.noIntrinsicMetric, height: itemH)}override init(frame: CGRect) {super.init(frame: frame)commonInit()}required init?(coder: NSCoder) {super.init(coder: coder)commonInit()heightAnchor.constraint(equalToConstant: 200).isActive = true}override func layoutSubviews() {super.layoutSubviews()updateItemsLayout()invalidateIntrinsicContentSize()}override func prepareForInterfaceBuilder() {count = 3}}private extension SequenceView {func commonInit() {count = 0backgroundColor = .gray}func updateItemsCount() {items.forEach {$0.removeFromSuperview() }items = []items = (0 ..< count).map {_ in createItem() }}func updateItemsLayout() {guard count > 0 else {return}let space: CGFloat = 10let itemW = (bounds.size.width - (CGFloat(count - 1)*space)) / CGFloat(count)itemH = itemWfor (index, item) in items.enumerated() {// 根据 控件当前自身宽度 计算 item 高度,设置 frame 。item.frame = CGRect(x: CGFloat(index)*(space + itemW), y: 0, width: itemW, height: itemH)}}func createItem() -> UIView {let view = UIView()view.backgroundColor = .yellowaddSubview(view)return view}}