仅使用intrinsicContentSize的UIStackView比例布局

18

我在UIStackView中排列子视图的布局遇到了问题,想知道是否有人可以帮助我理解发生了什么。

所以我有一个带有间距的UIStackView(例如1,但这并不重要)和.fillProportionally分布。我添加了仅具有1x1固有大小(可以是任何东西,只是方形视图)的排列子视图,并且我需要它们在stackView中按比例拉伸。

问题在于,如果我添加没有实际框架,仅具有固有大小的视图,则会得到错误的布局

错误的布局

否则,如果我添加具有相同大小的框架的视图,则一切都按预期工作,

正确的布局

但我真的不想设置视图的框架。

我非常确定这全部都是关于抱紧和压缩阻力优先级的问题,但是无法弄清楚正确答案是什么。

下面是一个Playground示例:

import UIKit
import PlaygroundSupport

class LView: UIView {

    // If comment this and leave only intrinsicContentSize - result is wrong
    convenience init() {
        self.init(frame: CGRect(x: 0, y: 0, width: 1, height: 1))
    }

    // If comment this and leave only convenience init(), then everything works as expected
    public override var intrinsicContentSize: CGSize {
        return CGSize(width: 1, height: 1)
    }
}

let container = UIView(frame: CGRect(x: 0, y: 0, width: 300, height: 300))
container.backgroundColor = UIColor.white

let sv = UIStackView()
container.addSubview(sv)


sv.leftAnchor.constraint(equalTo: container.leftAnchor).isActive = true
sv.rightAnchor.constraint(equalTo: container.rightAnchor).isActive = true
sv.topAnchor.constraint(equalTo: container.topAnchor).isActive = true
sv.bottomAnchor.constraint(equalTo: container.bottomAnchor).isActive = true
sv.translatesAutoresizingMaskIntoConstraints = false
sv.spacing = 1
sv.distribution = .fillProportionally

// Adding arranged subviews to stackView, 24 elements with intrinsic size 1x1
for i in 0..<24 {
    let a = LView()
    a.backgroundColor = (i%2 == 0 ? UIColor.red : UIColor.blue)
    sv.addArrangedSubview(a)
}
sv.layoutIfNeeded()

PlaygroundPage.current.liveView = container

如果我运行你的完全相同的代码,但将.fillProportionally更改为.fillEqually,我得到了你的第二张图片... 这是你想要的吗?(顺便说一句,在你的循环中不需要sv.layoutIfNeeded() - DonMag
.fillEqually实际上并不完全符合我的要求,因为我需要控制元素的宽度(在这个水平示例中),而.fillEqually将给出等宽列。我的意思是,我确实需要一些列比其他列大两倍,例如,还有一些列应该比那些大两倍的列大三倍。关于sv.layoutIfNeeded(),你是对的,是我的错,我会在这个例子中将其移出循环。 - Arthur Grishin
@Woof,不,那并不能解决问题。首先,当将视图添加到arrangedSubviews时,所有视图已经自动添加了宽度约束。其次,如果我自己设置宽度锚点,那么.fillProportionally就完全停止工作了,例如,视图不会根据stackView的宽度按比例缩放,因为它们有宽度约束,即使这个约束是lessOrEqual或greaterOrEqual。第三,如果每个视图已经具有自己的intrinsicContentSize,并且根据Apple文档,.fillProportionally依赖于intrinsicContentSize,那么为什么我还要自己设置宽度约束呢? - Arthur Grishin
1
经过一些测试...当.spacing不为零时(将其更改为sv.spacing = 0),似乎.fillProportionally会产生奇怪的结果,您将得到第二张图片,只是视图之间没有空间。我不知道这是否被认为是“错误”——还是一个“怪癖”。几乎可以认为自动布局正在将比例大小应用于空格——然后使用绝对值渲染它们。 - DonMag
1
但是,width约束应该由UIStackView自动设置,不是吗?当您将视图添加为arrangedSubview时,width约束基于添加的视图的intrisicContentSize,以及UIStackView中现有视图的数量和大小进行计算并应用到视图上。为什么我需要再次手动添加width约束?顺便提一下,如果我检查视图调试,最后一个具有最大宽度的元素,其width约束被禁用了。在我看来,这相当奇怪。 - Arthur Grishin
显示剩余8条评论
2个回答

44

很明显,这是UIStackView实现中的一个错误(即系统错误)。

DonMag 在他的评论中已经给出了指向正确方向的提示:

当你将堆栈视图的spacing设置为0时,一切都按预期工作。但是,当你将它设置为任何其他值时,布局就会破坏。


以下是解释原因:

ℹ️ 为了简单起见,我将假设堆栈视图具有

  • 水平轴
  • 10个排列的子视图

使用.fillProportionally分布,UIStackView创建如下系统约束:

  • 对于每个排列的子视图,它添加一个等宽的约束(UISV-fill-proportionally),该约束与堆栈视图本身相关联,并带有一个乘数

    arrangedSubview[i].width = multiplier[i] * stackView.width
    

    如果您在堆栈视图中有n个排列的子视图,您将获得n个这些约束条件。让我们称它们为proportionalConstraint[i](其中i表示arrangedSubviews数组中相应视图的位置)。

    这些约束条件是非必需的(即它们的优先级不是1000)。相反,第一个元素在arrangedSubviews数组中的约束条件被分配优先级999,第二个被分配优先级998等等:

    proportionalConstraint[0].priority = 999 
    proportionalConstraint[1].priority = 998 
    proportionalConstraint[2].priority = 997 
    proportionalConstraint[3].priority = 996
    ...         
    proportionalConstraint[n–1].priority = 1000 – n
    

    这意味着必需约束条件将始终优先于这些比例约束条件

  • 为了连接排列的子视图(可能带有间距),系统还会创建n-1个名为UISV-spacing的约束条件:

    arrangedSubview[i].trailing + spacing = arrangedSubview[i+1].leading
    

    这些约束是必需的(即优先级=1000)。

    系统还会创建一些其他的约束(例如垂直轴和将第一个和最后一个排列的子视图固定到堆栈视图的边缘),但我不会在此详细介绍它们,因为它们与理解出现的问题无关。

    苹果公司有关.fillProportionally分布的文档指出:

    一种布局,在此布局中,堆栈视图调整其排列视图大小,以使它们填充沿堆栈视图轴线的可用空间。根据堆栈视图轴线上的内在内容大小对视图进行比例调整。

    因此,根据此方式,对于spacing = 0,应计算proportionalConstraintmultiplier如下:

    • totalIntrinsicWidth = ∑iintrinsicWidth[i]
    • multiplier[i] = intrinsicWidth[i] / totalIntrinsicWidth

    如果我们的10个排列子视图都具有相同的内在宽度,则这将按预期工作:

    multiplier[i] = 0.1
    

    对于所有的proportionalConstraint。然而,一旦我们将间距更改为非零值,multiplier的计算就变得更加复杂,因为间距的宽度必须考虑在内。我已经进行了数学运算,multiplier[i]的公式如下:

    stack view应该如何计算乘数


    示例:

    对于以下配置的堆栈视图:

    • stackView.width = 400
    • stackView.spacing = 2

    上述方程将产生以下结果:

    multiplier[i] = 0.0955
    

    你可以通过将它们相加来证明这个正确性:

    (10 * width) + (9 * spacing)
        = (10 * multiplier * stackViewWidth) + (9 * spacing)
        = (10 * 0.0955 * 400) + (9 * 2)
        = (0.955 * 400) + 18
        = 382 + 18
        = 400
        = stackViewWidth
    

    然而,该系统分配了不同的值:

    multiplier[i] = 0.0917431
    

    总宽度为

    (10 * width) + (9 * spacing)
        = (10 * 0.0917431 * 400) + (9 * 2)
        = 384,97
        < stackViewWidth
    

    显然,这个值是错误的。

    因此,系统必须打破一个约束。当然,它会打破具有最低优先级的约束,即最后排列子视图项的 proportionalConstraint

    这就是为什么你截图中的最后一个排列的子视图被拉伸的原因。

    如果你尝试不同的间距和堆叠视图宽度,你会得到各种奇怪的布局。但是它们都有一个共同点:间距始终优先。(如果你将间距设置为较大的值,例如30或40,你只能看到前两个或三个排列的子视图,因为其余的空间被必要的间距完全占用。)


    总之:

    .fillProportionally分布仅在spacing = 0时才正常工作。

    对于其他间距,系统会创建带有错误乘数的约束。

    这将破坏布局,因为

    • 如果乘数小于应该的值,则其中一个排列的子视图(最后一个)必须被拉伸
    • 如果乘数大于应该的值,则必须压缩多个排列的子视图。

    唯一的解决办法是“滥用”普通的 UIView作为视图之间所需的固定宽度约束的间距。(正常情况下,UILayoutGuide是为此目的引入的,但你甚至也不能使用它们,因为你无法将布局指南添加到堆叠视图中。)

    我很抱歉由于这个错误,没有干净的解决方案来做到这一点。


这个问题在 iOS 15.3 中仍然存在吗? :o - CyberMew

2
截至至少Xcode 9.2版本,只要将初始化程序和内在内容大小注释掉,提供的游乐场就可以正常工作。在这种情况下,即使间距>0,比例填充也能按预期工作。
以下是一个间距为5的示例。

enter image description here

这似乎是有道理的,因为安排的子视图没有固有的内容大小,而StackView确定它们的宽度以比例填充指定的轴。
如果只启用初始化程序(而不是固有内容大小覆盖),那么我会得到这个结果,它与游乐场中的注释不匹配,所以我猜这种行为必须自问题发布以来发生了变化。

enter image description here

我不理解这种行为,因为在使用自动布局时手动设置框架应该被忽略。
如果只启用内在内容大小覆盖(而不是初始化程序),那么我会得到引发此帖子的问题图像(这里间距为5): enter image description here 基本上,设计现在存在冲突,无法实现,因为视图想要宽度为1点,由于指定了内在内容大小。这里的总空间应该是
24 views * 1 point/view + 23 spaces * 5 points/space = 139 < 300 = sv.bounds.width

由于Mischa指出了最低优先级的约束被打破,因此上一个排列视图的约束被打破了。
通过逐像素检查,前面的23个视图宽度都比1个像素宽,实际上是2或3个像素,除了最后一个视图,因此数学计算不太匹配,我不知道为什么,可能是十进制数字四舍五入的原因?
供参考,在这种情况下,它的内在内容大小为5个宽度,仍然无法满足约束条件。

enter image description here


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接