具有自定义约束的UIStackView-更改轴时“无法同时满足约束”

问题描述

我偶然发现了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,"<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,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,'|':rootView:0x7fe269c111a0 )>"
)

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

请注意约束:"<NSLayoutConstraint:0x6000008ba9e0 'UISV-canvas-connection' H:[viewA]-(0)-| (active,'|':stackView:0x7fe269d07b50 )>",其中表示“ viewA的尾随应与堆栈视图的尾随相同” 。这对于水平轴布局不是有效的约束。但这不是我添加的约束,它是堆栈视图(UISV-canvas-connection)内部的内容。

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

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

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

有什么想法可以认为它是错误的(并报告给Apple)以及是否还有其他(更好的)解决方案?

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

解决方法

如我所见,您和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
    }
}
,

虽然 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
    }
}

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

相关问答

依赖报错 idea导入项目后依赖报错,解决方案:https://blog....
错误1:代码生成器依赖和mybatis依赖冲突 启动项目时报错如下...
错误1:gradle项目控制台输出为乱码 # 解决方案:https://bl...
错误还原:在查询的过程中,传入的workType为0时,该条件不起...
报错如下,gcc版本太低 ^ server.c:5346:31: 错误:‘struct...