视图如何在不使用依赖于框架的 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 的行为非常相似:
当受 top、leading 和 trailing 边缘约束时,它应该根据其内容确定自己的自然高度。李> 当在所有边缘上受到约束时,它应该通过根据这些约束确定的展开和折叠来填充所有可用空间。 此外,我希望在视图内部不使用自动布局来实现这一目标。为了说明这一点,请考虑以下示例:
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 个子视图都有一个 intrinsicContentSize
和 120 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 的情况下根据其宽度的函数确定其高度?的主要内容,如果未能解决你的问题,请参考以下文章