【问题标题】:UIStackView with custom constraints - "Unable to simultaneously satisfy constraints" when changing axis具有自定义约束的 UIStackView - 更改轴时“无法同时满足约束”
【发布时间】:2020-12-09 13:59:14
【问题描述】:

我偶然发现了UIStackView 的问题。 它与 UIStackView 与其排列的子视图之间存在一些自定义约束的情况有关。更改堆栈视图的轴和自定义约束时,控制台中会出现错误消息[LayoutConstraints] Unable to simultaneously satisfy constraints.

这是一个代码示例,您可以将其粘贴到新项目中以观察问题:

import UIKit

public class ViewController: UIViewController {
    private var stateToggle = false

    private let viewA: UIView = {
        let view = UIView()
        view.translatesAutoresizingMaskIntoConstraints = false
        view.accessibilityIdentifier = "viewA"
        view.backgroundColor = .red
        return view
    }()

    private let viewB: UIView = {
        let view = UIView()
        view.translatesAutoresizingMaskIntoConstraints = false
        view.accessibilityIdentifier = "viewB"
        view.backgroundColor = .green
        return view
    }()

    private lazy var stackView: UIStackView = {
        let stackView = UIStackView(arrangedSubviews: [viewA, viewB])
        stackView.translatesAutoresizingMaskIntoConstraints = false
        stackView.distribution = .fill
        stackView.alignment = .fill
        stackView.accessibilityIdentifier = "stackView"
        return stackView
    }()

    private lazy var viewAOneFourthOfStackViewHightConstraint = viewA.heightAnchor.constraint(equalTo: stackView.heightAnchor, multiplier: 0.25)
    private lazy var viewAOneFourthOfStackViewWidthConstraint = viewA.widthAnchor.constraint(equalTo: stackView.widthAnchor, multiplier: 0.25)

    public override func viewDidLoad() {
        super.viewDidLoad()
        view.accessibilityIdentifier = "rootView"

        view.addSubview(stackView)

        NSLayoutConstraint.activate([
            stackView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
            stackView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor),
            stackView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor),
            stackView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor),
        ])

        let button = UIButton(type: .system)
        button.frame = .init(x: 50, y: 50, width: 100, height: 44)
        button.setTitle("toggle layout", for: .normal)
        button.tintColor = .white
        button.backgroundColor = .black
        button.addTarget(self, action: #selector(buttonTap), for: .touchUpInside)
        view.addSubview(button)

        updateConstraintsBasedOnState()
    }

    @objc func buttonTap() {
        stateToggle.toggle()
        updateConstraintsBasedOnState()
    }

    private func updateConstraintsBasedOnState() {
        switch (stateToggle) {
        case true:
            stackView.axis = .horizontal
            viewAOneFourthOfStackViewHightConstraint.isActive = false
            viewAOneFourthOfStackViewWidthConstraint.isActive = true
        case false:
            stackView.axis = .vertical
            viewAOneFourthOfStackViewWidthConstraint.isActive = false
            viewAOneFourthOfStackViewHightConstraint.isActive = true
        }
    }
}

在这个例子中,我们正在制作一个带有两个子视图的堆栈视图。我们希望其中一个占据 25% 的区域 - 所以我们有实现这一目标的限制(每个方向一个),但我们需要能够相应地切换它们。当切换发生时(本例是从垂直堆栈到水平堆栈),出现错误信息:

[LayoutConstraints] Unable to simultaneously satisfy constraints.
    Probably at least one of the constraints in the following list is one you don't want. 
    Try this: 
        (1) look at each constraint and try to figure out which you don't expect; 
        (2) find the code that added the unwanted constraint or constraints and fix it. 
(
    "<NSLayoutConstraint:0x60000089ca00 stackView.leading == UILayoutGuide:0x6000012b48c0'UIViewSafeAreaLayoutGuide'.leading   (active, names: stackView:0x7fe269d07b50 )>",
    "<NSLayoutConstraint:0x60000089c9b0 stackView.trailing == UILayoutGuide:0x6000012b48c0'UIViewSafeAreaLayoutGuide'.trailing   (active, names: stackView:0x7fe269d07b50 )>",
    "<NSLayoutConstraint:0x60000089c730 viewA.width == 0.25*stackView.width   (active, names: viewA:0x7fe269e14fd0, stackView:0x7fe269d07b50 )>",
    "<NSLayoutConstraint:0x6000008ba990 'UISV-canvas-connection' stackView.leading == viewA.leading   (active, names: stackView:0x7fe269d07b50, viewA:0x7fe269e14fd0 )>",
    "<NSLayoutConstraint:0x6000008ba9e0 'UISV-canvas-connection' H:[viewA]-(0)-|   (active, names: stackView:0x7fe269d07b50, viewA:0x7fe269e14fd0, '|':stackView:0x7fe269d07b50 )>",
    "<NSLayoutConstraint:0x6000008bab20 'UIView-Encapsulated-Layout-Width' rootView.width == 375   (active, names: rootView:0x7fe269c111a0 )>",
    "<NSLayoutConstraint:0x60000089ce60 'UIViewSafeAreaLayoutGuide-left' H:|-(0)-[UILayoutGuide:0x6000012b48c0'UIViewSafeAreaLayoutGuide'](LTR)   (active, names: rootView:0x7fe269c111a0, '|':rootView:0x7fe269c111a0 )>",
    "<NSLayoutConstraint:0x60000089cdc0 'UIViewSafeAreaLayoutGuide-right' H:[UILayoutGuide:0x6000012b48c0'UIViewSafeAreaLayoutGuide']-(0)-|(LTR)   (active, names: rootView:0x7fe269c111a0, '|':rootView:0x7fe269c111a0 )>"
)

Will attempt to recover by breaking constraint 
<NSLayoutConstraint:0x6000008ba990 'UISV-canvas-connection' stackView.leading == viewA.leading   (active, names: stackView:0x7fe269d07b50, viewA:0x7fe269e14fd0 )>

注意约束:"&lt;NSLayoutConstraint:0x6000008ba9e0 'UISV-canvas-connection' H:[viewA]-(0)-| (active, names: stackView:0x7fe269d07b50, viewA:0x7fe269e14fd0, '|':stackView:0x7fe269d07b50 )&gt;",它说 “viewA 的尾随应该与堆栈视图的尾随相同”。这不是水平轴布局的有效约束。但这不是我添加的约束,它是堆栈视图内部的东西 (UISV-canvas-connection)。

在我看来,因为我们正在激活/停用自定义约束,并且堆栈视图在切换轴时在内部执行相同的操作 - 暂时存在冲突和错误。

潜在的解决方案/变通方法:

  • 解决方法 1:使自定义约束具有 priority = 999。这不是一个好的解决方法 - 不仅因为它很 hacky,还因为在某些情况下它会导致其他布局问题(例如,当 viewA 有一些内部布局要求发生冲突时,例如需要拥抱优先级的子视图)。
  • 解决方法 2:在轴更改之前删除排列的视图,并在轴更改后重新添加它们。这可行,但它也很老套,对于一些复杂的情况在实践中可能会很困难。
  • 解决方法 3:根本不要使用 UIStackView - 使用常规 UIView 作为容器并根据需要创建所需的约束。

是否应将其视为错误(并报告给 Apple)以及是否有其他(更好的?)解决方案?

例如,有没有办法告诉 AutoLayout -“我要更改一些约束以及堆栈视图的轴 - 等我完成后再继续评估布局”。

【问题讨论】:

  • 写得很棒的问题,带有完全可重现的示例,解释了您尝试过的内容以及对可能解决方法的想法。我已经在这里呆了七年多了,也许还见过一个和这个一样好的问题。

标签: ios swift autolayout nslayoutconstraint uistackview


【解决方案1】:

在我看来,您和 iOS 都有您需要停用和激活的限制。为了正确执行此操作并避免冲突,您和 iOS 都需要先停用旧约束,然后才能开始激活新约束。

这是一个有效的解决方案。停用旧约束后,将stackView 告诉layoutIfNeeded()。然后它将按顺序获取其约束,您可以激活新的约束。

private func updateConstraintsBasedOnState() {
    switch (stateToggle) {
    case true:
        stackView.axis = .horizontal
        viewAOneFourthOfStackViewHightConstraint.isActive = false
        stackView.layoutIfNeeded()
        viewAOneFourthOfStackViewWidthConstraint.isActive = true
    case false:
        stackView.axis = .vertical
        viewAOneFourthOfStackViewWidthConstraint.isActive = false
        stackView.layoutIfNeeded()
        viewAOneFourthOfStackViewHightConstraint.isActive = true
    }
}

【讨论】:

  • Hmmm... .layoutSubviews() 的文档:您不应该直接调用此方法。 可以将其替换为 stackView.setNeedsLayout(); stackView.layoutIfNeeded(); 为每个viewAOneFourth... 约束赋予一个小于.required // 1000 的优先级。
  • 感谢@DonMag 的评论,我将其更改为layoutIfNeeded(),它仍然有效。
【解决方案2】:

虽然 vacawama 的回答可以解决问题,但还有两个选择...

1 - 为约束赋予小于.required 的优先级,以允许自动布局暂时打破约束而不会抱怨。

将此添加到viewDidLoad()

viewAOneFourthOfStackViewHightConstraint.priority = .defaultHigh
viewAOneFourthOfStackViewWidthConstraint.priority = .defaultHigh

并保留原来的 updateConstraintsBasedOnState(),或者

2 - 使用不同的优先级,并交换它们而不是停用/​​激活约束。

将此添加到viewDidLoad()

viewAOneFourthOfStackViewHightConstraint.priority = .defaultHigh
viewAOneFourthOfStackViewWidthConstraint.priority = .defaultLow
viewAOneFourthOfStackViewHightConstraint.isActive = true
viewAOneFourthOfStackViewWidthConstraint.isActive = true
    

并将updateConstraintsBasedOnState() 更改为:

private func updateConstraintsBasedOnState() {
    switch (stateToggle) {
    case true:
        stackView.axis = .horizontal
        viewAOneFourthOfStackViewHightConstraint.priority = .defaultLow
        viewAOneFourthOfStackViewWidthConstraint.priority = .defaultHigh
    case false:
        stackView.axis = .vertical
        viewAOneFourthOfStackViewWidthConstraint.priority = .defaultLow
        viewAOneFourthOfStackViewHightConstraint.priority = .defaultHigh
    }
}

我不知道改变优先级是否比改变激活更有效?但可能需要考虑。

【讨论】:

    猜你喜欢
    • 2020-11-15
    • 2012-12-28
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-02-09
    • 1970-01-01
    • 2014-10-27
    • 2014-06-19
    相关资源
    最近更新 更多