视图如何在不使用依赖于框架的 intrinsicContentSize 的情况下根据其宽度的函数确定其高度?

Posted

技术标签:

【中文标题】视图如何在不使用依赖于框架的 intrinsicContentSize 的情况下根据其宽度的函数确定其高度?【英文标题】:How can a view determine its height based on a function of its width without using an intrinsicContentSize that is dependent on the frame? 【发布时间】:2020-05-29 09:50:34 【问题描述】:

我试图弄清楚如何最好地创建一个根据给定宽度确定其自身高度的视图。我想要的行为与垂直 UIStackView 的行为非常相似:

当受 topleadingtrailing 边缘约束时,它应该根据其内容确定自己的自然高度。李> 当在所有边缘上受到约束时,它应该通过根据这些约束确定的展开和折叠来填充所有可用空间。 此外,我希望在视图内部不使用自动布局来实现这一目标。

为了说明这一点,请考虑以下示例:

class ViewController: UIViewController 

    let v = View()

    override func viewDidLoad() 
        super.viewDidLoad()

        v.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(v)

        NSLayoutConstraint.activate([
            v.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
            v.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            v.trailingAnchor.constraint(equalTo: view.trailingAnchor),

            // Toggle this constraint on and off.
            // v.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor),
        ])
    


class View: UIView 

    // Imagine this came from computing the size of some child views
    // and that it is relative to the width of the bounds.
    var contentHeight: CGFloat  bounds.width * 0.5 

    var boundsWidth: CGFloat = 0 
        didSet  if oldValue != boundsWidth  invalidateIntrinsicContentSize()  
    

    override var intrinsicContentSize: CGSize 
        .init(width: bounds.width, height: contentHeight)
    

    override func layoutSubviews() 
        super.layoutSubviews()
        boundsWidth = bounds.width
        backgroundColor = .red
    


这个例子实现了我上面描述的行为,但由于我使用intrinsicContentSize 的方式,我对此有很大的保留。

documentation for instrinsicContentSize 声明它必须独立于内容框架,我没有管理。我正在根据框架的宽度计算内在尺寸。

如何实现这种行为并且不让instrinsicContentSize 依赖边界?

【问题讨论】:

您提供了更多详细信息,但仍不清楚您要做什么。 “创建一个根据给定宽度确定其自身高度的视图” --- 你希望高度是宽度的百分比吗?您是否打算向此视图添加子视图,并且希望子视图的高度来确定视图的高度? 感谢您的回复!我——也许是错误地——跳过了这些点,因为我认为它们偏离了我的实际问题!我正在处理的特定视图将是我将在内部定位的许多较小子视图的容器。为了简化事情,你可以画一个垂直的 UIStackView。我在示例代码中使用了宽度的百分比,希望人们会忽略它并专注于更普遍的问题:视图如何根据宽度的一些计算来确定其高度而不使用依赖于框架的 intrinsicContentSize。 【参考方案1】:

这都可以通过自动布局约束来完成。无需计算任何东西。

您需要做的就是确保您的自定义视图的内容具有适当的约束来定义布局。

例如,UILabel 具有基于其文本的固有大小。您可以将“顶部”标签限制在视图的顶部,将“中间”标签限制在“顶部”标签的底部,将“底部”标签限制在“中间”标签的底部 and 到视图的底部。

这是一个示例(全部通过代码):

class SizeTestViewController: UIViewController 

    let v = ExampleView()

    override func viewDidLoad() 
        super.viewDidLoad()

        v.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(v)

        NSLayoutConstraint.activate([
            v.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
            v.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            v.trailingAnchor.constraint(equalTo: view.trailingAnchor),

        ])

        v.topLabel.text = "This is the top label."
        v.middleLabel.text = "This is a bunch of text for the middle label. Since we have it set to numberOfLines = 0, the text will wrap onto mutliple lines (assuming it needs to)."
        v.bottomLabel.text = "This is the bottom label text\nwith embedded newline characters\nso we can see the multiline feature without needing word wrap."

        // so we can see the view's frame
        v.backgroundColor = .red
    


class ExampleView: UIView 
    var topLabel: UILabel = 
        let v = UILabel()
        v.translatesAutoresizingMaskIntoConstraints = false
        v.backgroundColor = .yellow
        v.numberOfLines = 0
        return v
    ()

    var middleLabel: UILabel = 
        let v = UILabel()
        v.translatesAutoresizingMaskIntoConstraints = false
        v.backgroundColor = .cyan
        v.numberOfLines = 0
        return v
    ()

    var bottomLabel: UILabel = 
        let v = UILabel()
        v.translatesAutoresizingMaskIntoConstraints = false
        v.backgroundColor = .green
        v.numberOfLines = 0
        return v
    ()

    override init(frame: CGRect) 
        super.init(frame: frame)
        commonInit()
    
    required init?(coder: NSCoder) 
        super.init(coder: coder)
        commonInit()
    
    func commonInit() -> Void 

        addSubview(topLabel)
        addSubview(middleLabel)
        addSubview(bottomLabel)

        NSLayoutConstraint.activate([

            // constrain topLabel 8-pts from top, leading, trailing
            topLabel.topAnchor.constraint(equalTo: topAnchor, constant: 8.0),
            topLabel.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 8.0),
            topLabel.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -8.0),

            // constrain middleLabel 8-pts from topLabel
            //  8-pts from leading, trailing
            middleLabel.topAnchor.constraint(equalTo: topLabel.bottomAnchor, constant: 8.0),
            middleLabel.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 8.0),
            middleLabel.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -8.0),

            // constrain bottomLabel 8-pts from middleLabel
            //  8-pts from leading, trailing
            //  8-pts from bottom
            bottomLabel.topAnchor.constraint(equalTo: middleLabel.bottomAnchor, constant: 8.0),
            bottomLabel.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 8.0),
            bottomLabel.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -8.0),
            bottomLabel.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -8.0),

        ])

    


结果:

并旋转,因此您可以看到自动调整大小:


编辑

关于内在内容大小的一点说明...

在这张图片中,所有 5 个子视图都有一个 intrinsicContentSize120 x 80

class IntrinsicTestView: UIView 
    override var intrinsicContentSize: CGSize 
        return CGSize(width: 120, height: 80)
    

如你所见:

如果我添加约束以指定宽度 - 使用宽度约束或前导和尾随约束 - 视图将是120 分宽。 如果我添加约束以指定高度 - 使用高度约束或顶部和底部约束 - 视图将是身高 80 分。 否则,宽度和高度将由我添加的约束来确定。

这是该示例的完整代码:

class IntrinsicTestView: UIView 
    override var intrinsicContentSize: CGSize 
        return CGSize(width: 120, height: 80)
    


class IntrinsicExampleViewController: UIViewController 

    override func viewDidLoad() 
        super.viewDidLoad()

        var iViews: [IntrinsicTestView] = [IntrinsicTestView]()

        var v: IntrinsicTestView

        let colors: [UIColor] = [.red, .green, .blue, .yellow, .purple]

        colors.forEach  c in
            let v = IntrinsicTestView()
            v.backgroundColor = c
            v.translatesAutoresizingMaskIntoConstraints = false
            view.addSubview(v)
            iViews.append(v)
        

        let g = view.safeAreaLayoutGuide

        // first view at Top: 20 / Leading: 20
        v = iViews[0]
        v.topAnchor.constraint(equalTo: g.topAnchor, constant: 20.0).isActive = true
        v.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0).isActive = true

        // second view at Top: 120 / Leading: 20
        //  height: 20
        v = iViews[1]
        v.topAnchor.constraint(equalTo: g.topAnchor, constant: 120.0).isActive = true
        v.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0).isActive = true
        v.heightAnchor.constraint(equalToConstant: 20).isActive = true

        // third view at Top: 160 / Leading: 20
        //  height: 40 / width: 250
        v = iViews[2]
        v.topAnchor.constraint(equalTo: g.topAnchor, constant: 160.0).isActive = true
        v.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0).isActive = true
        v.heightAnchor.constraint(equalToConstant: 40).isActive = true
        v.widthAnchor.constraint(equalToConstant: 250).isActive = true

        // fourth view at Top: 220
        //  trailing: 20 / width: 250
        v = iViews[3]
        v.topAnchor.constraint(equalTo: g.topAnchor, constant: 220.0).isActive = true
        v.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0).isActive = true
        v.widthAnchor.constraint(equalToConstant: 250).isActive = true

        // fourth view at Top: 400 / Leading: 20
        //  trailing: 20 / bottom: 20
        v = iViews[4]
        v.topAnchor.constraint(equalTo: g.topAnchor, constant: 400.0).isActive = true
        v.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0).isActive = true
        v.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0).isActive = true
        v.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: -20.0).isActive = true

    


【讨论】:

太棒了,谢谢你的回答!你知道没有限制地解决同样问题的方法吗? @user1636130 - 这就是“自动布局”的定义。您使用约束来告诉自动布局引擎您希望如何在屏幕上排列视图和子视图。而且,通常,您这样做的方式是在不同的屏幕尺寸/方向上获得所需的布局。甚至不确定您会如何设想这种工作方式?您希望根据其内容(子视图)自己拥有一个视图大小,但您不想使用约束来告诉该视图如何自行调整大小? 在我的特殊情况下,子视图的定位非常复杂,因此值得我花时间探索基于框架的方法而不是使用自动布局。在这个特定场景中,它将产生更简单和可测试的逻辑。如果我能很好地理解如何使用intrinsicContentSize,那么我会在一个好地方:) @user1636130 - 一般来说,布局越“复杂”,约束就越有利。但是...如果您觉得情况并非如此...当您override var intrinsicContentSize 时,您只是在告诉自动布局“使用此大小,除非另有说明。”也许如果您发布一个真实示例来说明您正在尝试做什么? @user1636130 - 请参阅我的答案的编辑。我添加了一些关于内在内容大小的说明【参考方案2】:

正如 DonMag 在他的回复中所说,intrinsicContentSize 并非完全用于复杂的布局管理,而是在没有足够的框架约束时作为 View 首选大小的指示。

但是,我可以提供一个简单的示例,希望能够为您指明正确的方向。 假设我希望 RatioView 具有内在大小,使其高度是其宽度的线性函数:

let ratioView = RatioView()
ratioView.ratio = 0.5

在本例中,ratioView 的固有高度等于其宽度的一半。

RatioView 的代码如下:

class RatioView: UIView 
  var contentHeight: CGFloat  bounds.width * ratio 
  private var kvoObservation: NSKeyValueObservation?

  var ratio: CGFloat = 1 
    didSet  invalidateIntrinsicContentSize() 
  

  override var intrinsicContentSize: CGSize 
    .init(width: bounds.width, height: contentHeight)
  

  override func didMoveToSuperview() 
    guard let _ = superview else  return 
    kvoObservation = observe(\.frame, options: .initial)  [weak self] (_, change) in
      self?.invalidateIntrinsicContentSize()
    
  

  override func willMove(toSuperview newSuperview: UIView?) 
    if newSuperview == nil 
      kvoObservation = nil
    
  

上面的代码有两个重要的亮点:

视图调用invalidateIntrinsicContentSize 通知布局系统他的intrinsicContentSize 属性的值已更改。 视图通过KVO observation 持续监控他的frame 属性的任何变化,因此它可以在每次宽度变化时计算其新高度。

第二步特别值得注意:没有它,视图将不知道何时它的大小发生变化,因此没有机会通知布局系统(通过invalidateIntrinsicContentSize)它的intrinsicContentSize 已刷新。

【讨论】:

以上是关于视图如何在不使用依赖于框架的 intrinsicContentSize 的情况下根据其宽度的函数确定其高度?的主要内容,如果未能解决你的问题,请参考以下文章

如何在不使用 ConstraintLayout 中的边距属性的情况下相对于屏幕宽度设置 2 个视图之间的空间

在不使用垫片的情况下定位视图底部

如何在不使用某些框架的情况下在 php 中进行 MVC

SSM框架

初识 flask

如何在不裁剪行/子视图的情况下为 tableView 的大小调整设置动画?