iOS自定义View 控件自动计算size能力

iOS自定义View 控件自动计算size能力

背景

在使用 UILabel 和 UIImage 的时候,不用指定宽高约束,控件也不会报约束缺失,还可以根据内容自己确定适合的宽高,特别适合 Xib 和 Storyboard 布局。

我们在自定义控件的时候,怎么才能让控件具备这种自动计算宽高的功能了?

基本知识点

只需要自定义控件满足以下条件之一即可:

  1. 自定义控件实现intrinsicContentSize的重写并返回合适大小,适合子控件存在 frame初始化,或者动态添加。
  2. 自定义控件内部约束满足

这时候有部分长得帅的小伙伴要问了,如果自定义控件即满足了所有约束,又重写了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 调整周期如下:

调整contentSize
layoutSubviews 方法
updateConstraints 方法
初始化
intrinsicContentSize
invalidateIntrinsicContentSize 方法
约束结束

虚线表示可能调用, 可能某些步骤存在重复调用。

我们需要在合适的时机去调用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相关文档

疑问

  1. UILabel 的内部怎么计算出内在大小的?

即在intrinsicContentSize属性返回值是怎么计算的。按照打印日志来看,是经过多次(超过四次)updateLayout, intrinsicContenSize调用得出的。

  1. 自定义视图实现了 intrinsicContentSize,如何适配在UITableViewCell中自适应行高?

经过测试,自定义视图实现intrinsicContentSize,会使用第一次返回的内置大小,如果是通过多次invalidateIntrinsicContentSize来确定内置大小,在UITableViewCell中使用将不会达到想要的效果。

参考

Intrinsic Content Size of Multi-Line Text

iOS 中编码约束

实例

实现如下自定义控件,外部传入黄色 view 数量,等比例计算黄色高度,计算intrinsicContentSize

截屏2021-03-30 上午9.55.05

示例代码

override var intrinsicContentSize: CGSize {
     return CGSize(width: UIView.noIntrinsicMetric, height: itemH)
}

layoutSubviews

		override func layoutSubviews() {
        super.layoutSubviews()
        updateItemsLayout()
        invalidateIntrinsicContentSize()
    }

完整代码

import UIKit
@IBDesignable
class SequenceView: UIView {
    @IBInspectable
    var count: Int = 0 {
        didSet {
            updateItemsCount()
        }
    }

    private var items: [UIView] = []
    private var itemH: CGFloat = 0
    override 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 = 0
        backgroundColor = .gray
    }
    
    func updateItemsCount() {
        items.forEach { $0.removeFromSuperview() }
        items = []
        items = (0 ..< count).map { _ in createItem() }
    }
    
    func updateItemsLayout() {
        guard count > 0 else {
            return
        }
        let space: CGFloat = 10
        let itemW = (bounds.size.width - (CGFloat(count - 1)*space)) / CGFloat(count)
        itemH = itemW
        for (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 = .yellow
        addSubview(view)
        return view
    }
}

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
使用SwiftUI自定义iOS分段控件可以通过以下步骤实现: 1. 创建一个新的SwiftUI View,命名为SegmentedControl。 2. 在SegmentedControl中定义一个枚举类型,用于表示分段控件中的选项。 3. 在SegmentedControl中定义一个@Binding属性用于绑定选中的选项。 4. 在SegmentedControl中使用ForEach循环遍历所有的选项,并将它们显示在分段控件中。 5. 在ForEach循环中,使用Button显示每一个选项,并在按钮的action中更新选中的选项。 6. 为分段控件添加样式,例如设置选中的选项的背景色和字体颜色等。 下面是一个简单的示例代码: ```swift enum SegmentedOption: String, CaseIterable { case option1 case option2 case option3 } struct SegmentedControl: View { @Binding var selectedOption: SegmentedOption var body: some View { HStack { ForEach(SegmentedOption.allCases, id: \.self) { option in Button(action: { self.selectedOption = option }) { Text(option.rawValue) .foregroundColor(self.selectedOption == option ? .white : .black) .padding(.horizontal, 20) .padding(.vertical, 10) .background(self.selectedOption == option ? Color.blue : Color.gray) .cornerRadius(10) } } } } } ``` 在使用时,只需要将SegmentedControl添加到需要显示的View中,并将选中的选项绑定到某个属性即可。例如: ```swift struct ContentView: View { @State private var selectedOption: SegmentedOption = .option1 var body: some View { VStack { SegmentedControl(selectedOption: $selectedOption) Text("Selected option: \(selectedOption.rawValue)") } } } ``` 这样就可以在界面上显示一个自定义iOS分段控件了。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值