UIStackView比例布局只有intrinsicContentSize

Art*_*hin 12 ios autolayout swift uistackview

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

所以我有一些间距的UIStackView(例如1,但这没关系)和.fillProportionally分布.我添加的排列子视图只有1x1的intrinsicContentSize(可能是任何东西,只是方形视图),我需要在stackView中按比例拉伸它们.

问题是,如果我添加没有实际框架的视图,只有内在大小,那么我得到这个错误的布局

错误的布局

否则,如果我使用相同大小的帧添加视图,一切都按预期工作,

正确的布局

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

我很确定这是关于拥抱和压缩阻力优先的全部,但无法弄清楚正确答案是什么.

这是一个游乐场示例:

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
Run Code Online (Sandbox Code Playgroud)

Mis*_*cha 24

这显然是实现中的一个错误UIStackView(即系统错误).

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

将堆栈视图设置spacing为0时,一切都按预期工作.但是当您将其设置为任何其他值时,布局会中断.


以下解释原因:

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

  • 一个水平轴
  • 10个安排的子视图

随着.fillProportionally分发UIStackView创建系统约束如下:

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

    arrangedSubview[i].width = multiplier[i] * stackView.width
    
    Run Code Online (Sandbox Code Playgroud)

    如果在堆栈视图中有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
    
    Run Code Online (Sandbox Code Playgroud)

    这意味着所需的约束总是会胜过这些比例约束!

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

    arrangedSubview[i].trailing + spacing = arrangedSubview[i+1].leading
    
    Run Code Online (Sandbox Code Playgroud)

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

  • (系统还会创建一些其他约束(例如,对于垂直轴以及将第一个和最后一个排列的子视图固定到堆栈视图的边缘),但我不会在这里详细说明,因为它们与理解什么不相关出错了.)

Apple关于.fillProportionally发行的文档说明:

堆栈视图调整其排列视图大小的布局,以便它们沿堆栈视图的轴填充可用空间.视图根据堆栈视图轴的内在内容大小按比例调整大小.

所以根据这一点multiplier对于proportionalConstraintS的关系如下计算spacing = 0:

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

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

multiplier[i] = 0.1
Run Code Online (Sandbox Code Playgroud)

为了所有人proportionalConstraint.但是,只要我们将间距更改为非零值,计算multiplier就会变得复杂得多,因为必须考虑间距的宽度.我做的数学和公式multiplier[i]是:

堆栈视图应如何计算乘数


例:

对于堆栈视图配置如下:

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

上面的等式将产生:

multiplier[i] = 0.0955
Run Code Online (Sandbox Code Playgroud)

你可以通过添加它来证明这是正确的:

(10 * width) + (9 * spacing)
    = (10 * multiplier * stackViewWidth) + (9 * spacing)
    = (10 * 0.0955 * 400) + (9 * 2)
    = (0.955 * 400) + 18
    = 382 + 18
    = 400
    = stackViewWidth
Run Code Online (Sandbox Code Playgroud)

但是,系统会分配不同的值:

multiplier[i] = 0.0917431
Run Code Online (Sandbox Code Playgroud)

总计宽度为

(10 * width) + (9 * spacing)
    = (10 * 0.0917431 * 400) + (9 * 2)
    = 384,97
    < stackViewWidth
Run Code Online (Sandbox Code Playgroud)

显然,这个值是错误的.

因此,系统必须打破约束.当然,它打破了最低优先级的约束proportionalConstraint,即最后安排的子视图项.

这就是为什么屏幕截图中最后排列的子视图被拉伸的原因.

如果您尝试不同的间距并堆叠视图宽度,您将最终得到各种奇怪的布局.但它们都有一个共同点: 间距始终优先.(如果将间距设置为更大的值,如30或40,则只能看到前两个或三个排列的子视图,因为剩余的空间完全被所需的间距占据.)


总结一下:

.fillProportionally分配只有正常工作spacing = 0.

对于其他间距,系统会使用不正确的乘数创建约束.

这打破了布局

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

唯一的出路是"滥用" UIView具有所需固定宽度约束的普通s作为视图之间的间距.(通常,UILayoutGuide为此目的引入了s,但您甚至无法使用它们,因为您无法在堆栈视图中添加布局指南.)

我担心由于这个错误,没有干净的解决方案来做到这一点.