为什么具有单个视图、按比例填充和布局边距的 UIStackView 会导致模糊约束错误?

Why does a UIStackView with a single view, fill Proportionally, and Layout Margins causes ambiguous constraint error?

以下代码尝试将 UIStackView 添加到视图控制器,固定在所有边上并留有一点边距,并向其添加标签。

我希望 StackView 使用 .fillProportionally 作为它的分发模式,以备我稍后向其中添加更多视图时使用。

似乎对于单个排列的子视图只要分布模式是.fillProportionallylayout使用 margins 然后我得到一个不明确的约束错误(如下)。这个错误的原因是什么?

override func viewDidLoad() {
    super.viewDidLoad()

    let label = UILabel(frame: .zero)
    label.text = "ABC"

    let stack = UIStackView(arrangedSubviews: [label])
    stack.translatesAutoresizingMaskIntoConstraints = false
    stack.distribution = .fillProportionally
    stack.isLayoutMarginsRelativeArrangement = true
    stack.layoutMargins = .init(top: 10, left: 10, bottom: 10, right: 10)
    view.addSubview(stack)

    NSLayoutConstraint.activate([
        stack.centerXAnchor.constraint(equalTo: view.centerXAnchor),
        stack.centerYAnchor.constraint(equalTo: view.centerYAnchor),
        stack.widthAnchor.constraint(equalTo: view.widthAnchor),
        stack.heightAnchor.constraint(equalTo: view.heightAnchor),
    ])
}

}

模糊约束错误(WTFAutoLayout):

(
    "<NSLayoutConstraint:0x600001a432f0 'UISV-canvas-connection' UILayoutGuide:0x6000000f3100'UIViewLayoutMarginsGuide'.leading == UILabel:0x7ff470913280'ABC'.leading   (active)>",
    "<NSLayoutConstraint:0x600001a423f0 'UISV-canvas-connection' UILayoutGuide:0x6000000f3100'UIViewLayoutMarginsGuide'.trailing == UILabel:0x7ff470913280'ABC'.trailing   (active)>",
    "<NSLayoutConstraint:0x600001a425d0 'UISV-fill-proportionally' UILabel:0x7ff470913280'ABC'.width == UIStackView:0x7ff46d510030.width   (active)>",
    "<NSLayoutConstraint:0x600001a77f70 'UIView-leftMargin-guide-constraint' H:|-(10)-[UILayoutGuide:0x6000000f3100'UIViewLayoutMarginsGuide'](LTR)   (active, names: '|':UIStackView:0x7ff46d510030 )>",
    "<NSLayoutConstraint:0x600001a42940 'UIView-rightMargin-guide-constraint' H:[UILayoutGuide:0x6000000f3100'UIViewLayoutMarginsGuide']-(10)-|(LTR)   (active, names: '|':UIStackView:0x7ff46d510030 )>"
)

Will attempt to recover by breaking constraint 
<NSLayoutConstraint:0x600001a423f0 'UISV-canvas-connection' UILayoutGuide:0x6000000f3100'UIViewLayoutMarginsGuide'.trailing == UILabel:0x7ff470913280'ABC'.trailing   (active)>

首先,将labeltranslatesAutoresizingMaskIntoConstraints设为false

label.translatesAutoresizingMaskIntoConstraints = false

由于要将layoutMargins全部设为10,所以stackwidthheight不能等于view's widthheight

您需要在 widthheight 中容纳 20(双方)的差异。

所以 constraints 应该是,

NSLayoutConstraint.activate([
    stack.centerXAnchor.constraint(equalTo: view.centerXAnchor),
    stack.centerYAnchor.constraint(equalTo: view.centerYAnchor),
    stack.widthAnchor.constraint(equalTo: view.widthAnchor, multiplier: 1, constant: -20),
    stack.heightAnchor.constraint(equalTo: view.heightAnchor, multiplier: 1, constant: -20),
])

另外,margin spacing包含在constraints之内,不需要写下面的代码。所以删除它。

stack.isLayoutMarginsRelativeArrangement = true
stack.layoutMargins = .init(top: 10, left: 10, bottom: 10, right: 10)

编辑澄清,因为我原来的答案并不完全正确...

首先,UIStackView.fillProportionally 分布 属性 经常被误解。

其次,当堆栈视图的分布 .fillProportionally 堆栈视图的 .spacing 不是 0,或者当堆栈视图 设置了 .layoutMargins 时。

您遇到的问题是自动布局计算比例大小的方式。

根据实验,自动布局计算视图的比例宽度,然后然后应用布局边距,减去从最后一个视图的宽度来容纳space.

这可以很容易地证明如下:

有 6 个水平堆栈视图,每个设置为 200 点宽,分布设置为 .fillProportionally,并填充一个或两个视图。红色视图的固有宽度为 25,绿色视图为 75。

第一个堆栈视图,具有单个视图且没有布局边距,按预期填充宽度...红色视图占据 100% space。

第二个堆栈视图,有两个视图,没有布局边距,也按预期填充...红色视图为 50 磅宽 (25%),绿色视图为 150 磅宽 (75%) .

但是,第三个堆栈视图开始显示问题。单个视图的比例宽度为 100%,或 200 点……但随后应用了布局边距。这会将视图从左侧移动 10 磅,但是因为自动布局不会从 first 子视图中减去 space,所以它实际上超出了 10 磅的边缘堆栈视图(所以红色视图是 仍然 200 点宽)。

第四个堆栈视图看起来就像它在做我们想要的......每侧边距为 10 磅的比例填充......红色视图为 50 磅宽(200 的 25%)但是 绿色视图只有 130 点宽。所以自动布局给两个视图 50 点 (25%) 和 150 点 (75%) 但是 然后它应用边距 和离开绿色视图 20 分。

为底部的两个堆栈视图使用 left: 100 right: 0left: 0 right: 100 的布局边距使其更加明显。同样,对于其中的每一个,红色获得 50 分 (25%),绿色获得 150 分 (75%),但是随后 100 分的余量从绿色中剥离。


所以,要回答最初的问题,即当我们只有一个排列的子视图 and 布局边距时,为什么我们会得到不明确的约束,看在堆栈视图 3. 自动布局无法给红色 100% 的 space and 应用边距,所以它抛出布局错误。


这是 运行 上述示例的代码。如果您注释掉第三个堆栈视图,您不会得到错误:

class ProportionalStackExampleViewController: UIViewController {

    let outerStackView: UIStackView = {
        let v = UIStackView()
        v.translatesAutoresizingMaskIntoConstraints = false
        v.axis = .vertical
        v.spacing = 8
        return v
    }()

    let outerStackFrame: UIView = {
        let v = UIView()
        v.translatesAutoresizingMaskIntoConstraints = false
        v.layer.borderWidth = 0.5
        v.layer.borderColor = UIColor.blue.cgColor
        return v
    }()

    let infoLabel: UILabel = {
        let v = UILabel()
        v.translatesAutoresizingMaskIntoConstraints = false
        v.font = UIFont.systemFont(ofSize: 12.0, weight: .light)
        v.numberOfLines = 0
        v.textAlignment = .center
        v.text = "Red views have intrinsic width of 25\nGreen views have intrinsic width of 75\nAll horizontal stack views are 200-pts wide\nTap any view to see its width"
        return v
    }()

    let sizeLabel: UILabel = {
        let v = UILabel()
        v.translatesAutoresizingMaskIntoConstraints = false
        v.text = "(width)"
        return v
    }()

    let myGreen = UIColor(red: 0, green: 0.75, blue: 0, alpha: 1.0)

    override func viewDidLoad() {
        super.viewDidLoad()

        for _ in 1...6 {
            let lbl = UILabel()
            lbl.font = UIFont.systemFont(ofSize: 12.0, weight: .light)
            lbl.numberOfLines = 0
            lbl.textAlignment = .center
            outerStackView.addArrangedSubview(lbl)
            let sv = UIStackView()
            sv.translatesAutoresizingMaskIntoConstraints = false
            sv.axis = .horizontal
            sv.distribution = .fillProportionally
            sv.spacing = 0
            outerStackView.addArrangedSubview(sv)
        }

        view.addSubview(infoLabel)
        view.addSubview(sizeLabel)
        view.addSubview(outerStackFrame)
        view.addSubview(outerStackView)

        let g = view.safeAreaLayoutGuide

        NSLayoutConstraint.activate([

            infoLabel.topAnchor.constraint(equalTo: g.topAnchor, constant: 20.0),
            infoLabel.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
            infoLabel.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),

            sizeLabel.topAnchor.constraint(equalTo: infoLabel.bottomAnchor, constant: 20.0),
            sizeLabel.centerXAnchor.constraint(equalTo: g.centerXAnchor),

            outerStackView.topAnchor.constraint(equalTo: sizeLabel.bottomAnchor, constant: 20.0),
            outerStackView.widthAnchor.constraint(equalToConstant: 200.0),
            outerStackView.centerXAnchor.constraint(equalTo: view.centerXAnchor),

            outerStackFrame.widthAnchor.constraint(equalTo: outerStackView.widthAnchor),
            outerStackFrame.heightAnchor.constraint(equalTo: outerStackView.heightAnchor),
            outerStackFrame.centerXAnchor.constraint(equalTo: outerStackView.centerXAnchor),
            outerStackFrame.centerYAnchor.constraint(equalTo: outerStackView.centerYAnchor),
        ])

        // StackView 1
        if let lbl = outerStackView.arrangedSubviews[0] as? UILabel,
            let sv = outerStackView.arrangedSubviews[1] as? UIStackView {

            lbl.text = "One view, no layoutMargins"

            var v = ProportionalView()
            v.w = 25.0
            v.backgroundColor = .red
            sv.addArrangedSubview(v)

            var tg = UITapGestureRecognizer(target: self, action: #selector(showWidth(_:)))
            v.addGestureRecognizer(tg)

        }

        // StackView 2
        if let lbl = outerStackView.arrangedSubviews[2] as? UILabel,
            let sv = outerStackView.arrangedSubviews[3] as? UIStackView {

            lbl.text = "Two views, no layoutMargins"

            var v = ProportionalView()
            v.w = 25.0
            v.backgroundColor = .red
            sv.addArrangedSubview(v)

            var tg = UITapGestureRecognizer(target: self, action: #selector(showWidth(_:)))
            v.addGestureRecognizer(tg)

            v = ProportionalView()
            v.w = 75.0
            v.backgroundColor = myGreen
            sv.addArrangedSubview(v)

            tg = UITapGestureRecognizer(target: self, action: #selector(showWidth(_:)))
            v.addGestureRecognizer(tg)
        }

        // comment out this block to see the auto-layout error goes away
        // StackView 3
        if let lbl = outerStackView.arrangedSubviews[4] as? UILabel,
            let sv = outerStackView.arrangedSubviews[5] as? UIStackView {

            lbl.text = "One view\nlayoutMargins left: 10 right: 10"

            var v = ProportionalView()
            v.w = 25.0
            v.backgroundColor = .red
            sv.addArrangedSubview(v)

            sv.isLayoutMarginsRelativeArrangement = true
            sv.layoutMargins = .init(top: 0, left: 10, bottom: 0, right: 10)

            var tg = UITapGestureRecognizer(target: self, action: #selector(showWidth(_:)))
            v.addGestureRecognizer(tg)

        }

        // StackView 4
        if let lbl = outerStackView.arrangedSubviews[6] as? UILabel,
            let sv = outerStackView.arrangedSubviews[7] as? UIStackView {

            lbl.text = "Two views\nlayoutMargins left: 10 right: 10"

            var v = ProportionalView()
            v.w = 25.0
            v.backgroundColor = .red
            sv.addArrangedSubview(v)

            var tg = UITapGestureRecognizer(target: self, action: #selector(showWidth(_:)))
            v.addGestureRecognizer(tg)

            v = ProportionalView()
            v.w = 75.0
            v.backgroundColor = myGreen
            sv.addArrangedSubview(v)

            sv.isLayoutMarginsRelativeArrangement = true
            sv.layoutMargins = .init(top: 0, left: 10, bottom: 0, right: 10)

            tg = UITapGestureRecognizer(target: self, action: #selector(showWidth(_:)))
            v.addGestureRecognizer(tg)
        }

        // StackView 5
        if let lbl = outerStackView.arrangedSubviews[8] as? UILabel,
            let sv = outerStackView.arrangedSubviews[9] as? UIStackView {

            lbl.text = "layoutMargins left: 100 right: 0"

            var v = ProportionalView()
            v.w = 25.0
            v.backgroundColor = .red
            sv.addArrangedSubview(v)

            var tg = UITapGestureRecognizer(target: self, action: #selector(showWidth(_:)))
            v.addGestureRecognizer(tg)

            v = ProportionalView()
            v.w = 75.0
            v.backgroundColor = myGreen
            sv.addArrangedSubview(v)

            sv.isLayoutMarginsRelativeArrangement = true
            sv.layoutMargins = .init(top: 0, left: 100, bottom: 0, right: 0)

            tg = UITapGestureRecognizer(target: self, action: #selector(showWidth(_:)))
            v.addGestureRecognizer(tg)
        }

        // StackView 6
        if let lbl = outerStackView.arrangedSubviews[10] as? UILabel,
            let sv = outerStackView.arrangedSubviews[11] as? UIStackView {

            lbl.text = "layoutMargins left: 0 right: 100"

            var v = ProportionalView()
            v.w = 25.0
            v.backgroundColor = .red
            sv.addArrangedSubview(v)

            var tg = UITapGestureRecognizer(target: self, action: #selector(showWidth(_:)))
            v.addGestureRecognizer(tg)

            v = ProportionalView()
            v.w = 75.0
            v.backgroundColor = myGreen
            sv.addArrangedSubview(v)

            sv.isLayoutMarginsRelativeArrangement = true
            sv.layoutMargins = .init(top: 0, left: 0, bottom: 0, right: 100)

            tg = UITapGestureRecognizer(target: self, action: #selector(showWidth(_:)))
            v.addGestureRecognizer(tg)
        }

    }

    @objc func showWidth(_ sender: UITapGestureRecognizer) -> Void {
        if let v = sender.view {
            sizeLabel.text = "Width: \(v.frame.width)"
            sizeLabel.textColor = v.backgroundColor
        }
    }

}

class ProportionalView: UIView {
    var w: CGFloat = 1.0

    override var intrinsicContentSize: CGSize {
        return CGSize(width: w, height: 40.0)
    }

}