为什么一个 UIStackView 单视图,按比例填充,Layout Margins 会导致不明确的约束错误?

use*_*130 3 uikit ios autolayout swift uistackview

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

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

似乎对于单个排列的子视图,无论何时使用分布模式 .fillProportionally布局边距,我都会得到一个不明确的约束错误(如下)。这个错误的原因是什么?

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

}

不明确的约束错误(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)>
Run Code Online (Sandbox Code Playgroud)

Don*_*Mag 8

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

首先,a 的.fillProportionally分布特性UIStackView经常被误解。

其次,当堆栈视图的分布是.fillProportionally 并且堆栈视图的.spacing不是0,或者堆栈视图已经.layoutMargins设置时,我们会得到一些奇怪的东西。

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

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

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

在此处输入图片说明

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

第一个堆栈视图,具有单个视图且没有布局边距,按预期填充宽度……红色视图占用了 100% 的空间。

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

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

第四个堆栈视图看起来像是在做我们想要的……按比例填充每边 10 磅的边距……红色视图宽 50 磅(200 的 25%),绿色视图只有 130 磅宽的。所以自动布局给了两个视图 50-pts (25%) 和 150-pts (75%),但随后它应用了边距并将 20-pts 从绿色视图中移开。

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


因此,要回答有关为什么当我们有一个单独排列的子视图布局边距时会得到模糊约束的原始问题,请查看堆栈视图 3。自动布局无法为 Red 提供 100% 的空间应用边距,因此它引发布局错误。


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

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)
    }

}
Run Code Online (Sandbox Code Playgroud)