【问题标题】:Why does a UIStackView with a single view, fill Proportionally, and Layout Margins causes ambiguous constraint error?为什么单视图的UIStackView,按比例填充,布局边距会导致模糊约束错误?
【发布时间】:2020-08-24 01:09:17
【问题描述】:

以下代码尝试将 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),
    ])
}

}

模糊约束错误(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)>

【问题讨论】:

  • 你有没有找到解决这个问题的方法?虽然我理解 DonMag 在下面对为什么会发生这种情况的出色解释,但我无法开始思考如何解决它或解决它。

标签: ios swift autolayout uikit uistackview


【解决方案1】:

由于我最初的答案并不完全正确,因此需要进行澄清编辑...

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

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

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

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

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

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

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

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

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

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

为底部的两个堆栈视图使用left: 100 right: 0left: 0 right: 100 的布局边距使其更加明显。同样,对于其中的每一个,Red 获得 50 分 (25%),Green 获得 150 分 (75%),但随后 Green 的 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)
    }

}

【讨论】:

  • ? 多么棒的答案。这正是我所缺少的洞察力,谢谢!
  • 很好的解释。这几天我一直在试图理解这一点。我有一个类似的场景,虽然它是具有多个子视图的内部堆栈视图,但是当嵌入到滚动视图中然后作为 childVC 添加到 viewController 时,比例约束开始打破。仍然不知道如何修复,但至少我明白它为什么会坏
  • @Anthony - 当你说 “比例约束开始打破” ...你的意思是子视图约束彼此成比例吗?或者你想在堆栈视图上使用Fill Proportionally
  • @DonMag - 我尝试过以不同的方式进行布局。最好的似乎可行,但给了我 UISV 间距约束冲突。乐于分享代码,提出单独的问题或 DM
  • @Anthony - 要么将其作为一个新问题发布,要么将代码发布到某个地方(GitHub、pastebin 等),我会看看。
【解决方案2】:

首先,将labeltranslatesAutoresizingMaskIntoConstraints设置为false

label.translatesAutoresizingMaskIntoConstraints = false

由于要将所有layoutMargins设置为10,所以stackwidthheight不能等于view'swidthheight

您需要在 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),
])

此外,边距包含在约束中,无需编写以下代码。所以删除它。

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

【讨论】:

  • 感谢您的回答!我特别在寻找 Layout Margins 和 fillProportional 影响的解决方案,而不是解决它的方法。例如,如果我试图通过继承 UIStackView 而不是嵌入它来实现相同的布局,那么我不想触及宽度和高度约束的常量
猜你喜欢
  • 2021-06-13
  • 1970-01-01
  • 1970-01-01
  • 2016-08-02
  • 1970-01-01
  • 1970-01-01
  • 2010-10-11
  • 1970-01-01
  • 2018-06-01
相关资源
最近更新 更多