ios - UIStackView 比例布局,仅具有内在内容大小

标签 ios swift autolayout uistackview

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

所以我有一些间距(例如 1,但这并不重要)和 .fillProportionally 分布的 UIStackView。我正在添加只有 1x1 的内在内容大小(可以是任何东西,只是方形 View )的排列 subview ,我需要它们在 stackView 中按比例拉伸(stretch)。

问题是,如果我添加没有实际框架的 View ,只有内在尺寸,那么我会得到这个错误的布局

wrong layout

否则,如果我添加具有相同大小框架的 View ,一切都会按预期进行,

correct layout

但我真的不想设置 View 的框架。

我很确定这完全是关于拥抱和抗压优先,但无法弄清楚正确的答案是什么。

这是一个游乐场示例:

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

最佳答案

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

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

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

原因如下:

ℹ️ 为简单起见,我假设堆栈 View 具有

  • 水平轴和
  • 10 个排列的 subview

  • .fillProportionally分销UIStackView创建系统约束如下:
  • 对于每个排列的 subview ,它添加一个 等宽与堆栈 View 本身相关的约束 ( UISV-fill-proportionally ) 乘数 :
    arrangedSubview[i].width = multiplier[i] * stackView.width
    

    如果您在堆栈 View 中有 n 个排列的 subview ,您将获得 n 个这些约束。让我们调用他们 proportionalConstraint[i] (其中 i 表示相应 View 在 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
    

    这意味着 所需的约束总是胜过这些比例约束 !
  • 为了连接排列好的 subview (可能有间距),系统还创建了 n–1 个约束,称为 UISV-spacing :
    arrangedSubview[i].trailing + spacing = arrangedSubview[i+1].leading
    

    这些约束是必需的(即优先级 = 1000)。
  • (系统还将创建一些其他约束(例如,对于垂直轴以及将第一个和最后一个排列的 subview 固定到堆栈 View 的边缘)但我不会在这里详细介绍,因为它们与理解什么无关出错了。)

  • Apple's documentation.fillProportionally分布状态:

    A layout where the stack view resizes its arranged views so that they fill the available space along the stack view’s axis. Views are resized proportionally based on their intrinsic content size along the stack view’s axis.



    所以根据这个multiplierproportionalConstraint s 应按如下方式计算 spacing = 0 :
  • totalIntrinsicWidth = ∑i intrinsicWidth[i]
  • multiplier[i] = intrinsicWidth[i]/totalIntrinsicWidth

  • 如果我们排列的 10 个 subview 都具有相同的内在宽度,这将按预期工作:
    multiplier[i] = 0.1
    

    所有 proportionalConstraint s。但是,一旦我们将间距更改为非零值,multiplier 的计算变得更加复杂,因为必须考虑间距的宽度。我已经完成了 multiplier[i] 的数学和公式是:

    How the stack view should compute the multiplier

    例子:

    对于配置如下的堆栈 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最后排列的 subview 项。

    这就是屏​​幕截图中最后一个排列的 subview 被拉伸(stretch)的原因。

    如果您尝试不同的间距和堆栈 View 宽度,您最终会得到各种看起来很奇怪的布局。但他们都有一个共同点:
    间距始终优先。 (如果您将间距设置为更大的值,例如 30 或 40,您将只会看到前两个或三个排列的 subview ,因为其余空间已被所需的间距完全占据。)

    总结一下:
    .fillProportionally分发仅适用于 spacing = 0 .

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

    这打破了布局
  • 如果乘数小于它应该是
  • ,任何一个排列的 subview (最后一个)必须被拉伸(stretch)
  • 如果乘数大于应有的值,则必须压缩多个排列的 subview 。

  • 解决这个问题的唯一方法是“滥用”普通 UIView s 具有所需的固定宽度约束作为 View 之间的间距。 (通常, UILayoutGuide 是为此目的引入的,但您甚至不能使用它们,因为您无法向堆栈 View 添加布局指南。)

    恐怕由于这个错误,没有干净的解决方案来做到这一点。

    关于ios - UIStackView 比例布局,仅具有内在内容大小,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/46253248/

    相关文章:

    ios - 使用 alamofire 通过 iOS 应用程序连接 Windows SSL 服务器

    ios - 多次将完全相同的 NSLayoutConstraints 附加到同一个 UIView 会发生什么?

    ios - 如何将电话号码和电子邮件添加到地址簿

    iphone - 得到当前的纬度和经度

    objective-c - insertItemsAtIndexPaths 更新错误

    swift - Set 如何解决哈希冲突?

    swift - 如何初始化scrollView分页的起始索引?

    ios - 使用 reloadRowsAtIndexPaths 或 reloadSections 导致 Tableview Cell 闪烁

    swift - 如何为 Xib 设置单元格动态高度的约束...?

    ios - UIImageView 和自动布局