当 trait collection 发生变化时,会出现约束冲突,就好像 stackview 轴没有变化一样

When trait collection changes, constraint conflicts arise as though the stackview axis didn't change

我有一个带有两个控件的堆栈视图。

当UI没有垂直约束时: Vertical1

当UI垂直受限时:Horizontal1

我得到了如图所示的两个 UI。当我第一次显示 UIs 时,没有约束冲突。但是,当我从垂直约束变为 vertical = regular 时,我遇到了约束冲突。

当我注释掉 stackview space(见下面的代码注释)时,我没有遇到约束冲突。

class ViewController: UIViewController {

    var rootStack: UIStackView!
    var aggregateStack: UIStackView!
    var field1: UITextField!
    var field2: UITextField!
    var f1f2TrailTrail: NSLayoutConstraint!

    override func viewDidLoad() {

        super.viewDidLoad()
        view.backgroundColor = .white
        createIntializeViews()
        createInitializeAddStacks()
    }

    private func createIntializeViews() {

        field1 = UITextField()
        field2 = UITextField()
        field1.text = "test 1"
        field2.text = "test 2"
    }

    private func createInitializeAddStacks() {

        rootStack = UIStackView()             

        aggregateStack = UIStackView()

        // If I comment out the following, there are no constraint conflicts
        aggregateStack.spacing = 2            

        aggregateStack.addArrangedSubview(field1)
        aggregateStack.addArrangedSubview(field2)
        rootStack.addArrangedSubview(aggregateStack)

        view.addSubview(rootStack)

        rootStack.translatesAutoresizingMaskIntoConstraints = false
        aggregateStack.translatesAutoresizingMaskIntoConstraints = false
        field1.translatesAutoresizingMaskIntoConstraints = false
        field2.translatesAutoresizingMaskIntoConstraints = false

        f1f2TrailTrail = field2.trailingAnchor.constraint(equalTo: field1.trailingAnchor)
    }


    override public func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {

        super.traitCollectionDidChange(previousTraitCollection)

        if traitCollection.verticalSizeClass == .regular {
            aggregateStack.axis = .vertical
            f1f2TrailTrail.isActive = true
        } else if traitCollection.verticalSizeClass == .compact {
            f1f2TrailTrail.isActive = false
            aggregateStack.axis = .horizontal
        } else {
            print("Unexpected")
        }
    }
}

约束冲突在这里-

(
    "<NSLayoutConstraint:0x600001e7d1d0 UITextField:0x7f80b2035000.trailing == UITextField:0x7f80b201d000.trailing   (active)>",
    "<NSLayoutConstraint:0x600001e42800 'UISV-spacing' H:[UITextField:0x7f80b201d000]-(2)-[UITextField:0x7f80b2035000]   (active)>"
)

Will attempt to recover by breaking constraint 
    <NSLayoutConstraint:0x600001e42800 'UISV-spacing' H:[UITextField:0x7f80b201d000]-(2)-[UITextField:0x7f80b2035000]   (active)>

当我将输出放在 www.wtfautolayout.com 中时,我得到以下信息: Easier to Read Output

上图中显示的第二个约束让我认为在评估约束之前没有发生对 stackview 垂直轴的更改。

谁能告诉我我做错了什么或如何正确设置它(最好没有故事板)?

[编辑] 文本字段的后缘对齐,因此:

More of the form - portrait

More of the form - landscape

UIView 添加到 UIStackView 时,stackView 将根据分配给 stackView 的属性(axisalignment 为该视图分配约束, distribution, spacing).正如@DonMag 所提到的,您正在向 aggregateStack 视图中的 textField 添加约束。 aggregateStack 将根据它的属性添加自己的约束。通过删除该约束和 activation/deactivation 代码,约束冲突就消失了。

我使用您的代码创建了一个小示例,并向 stackView 添加了一些背景视图,以便您可以更轻松地查看更改各种属性时发生的情况。只是为了说明,我将 rootStackView 固定到视图控制器视图的边缘,这样它就可见了。

import UIKit

class StackViewController: UIViewController {

    var rootStack: UIStackView!
    var aggregateStack: UIStackView!
    var field1: UITextField!
    var field2: UITextField!
    var f1f2TrailTrail: NSLayoutConstraint!

    private lazy var backgroundView: UIView = {
        let view = UIView()
        view.backgroundColor = .purple
        view.layer.cornerRadius = 10.0
        return view
    }()

    private lazy var otherBackgroundView: UIView = {
        let view = UIView()
        view.backgroundColor = .green
        view.layer.cornerRadius = 10.0
        return view
    }()

    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = .white
        createIntializeViews()
        createInitializeAddStacks()
    }

    private func createIntializeViews() {

        field1 = UITextField()
        field1.backgroundColor = .orange
        field2 = UITextField()
        field2.backgroundColor = .blue
        field1.text = "test 1"
        field2.text = "test 2"
    }

    private func createInitializeAddStacks() {

        rootStack = UIStackView()
        rootStack.alignment = .center
        rootStack.distribution = .fillProportionally
        pinBackground(backgroundView, to: rootStack)

        aggregateStack = UIStackView()
        aggregateStack.alignment = .center
        aggregateStack.distribution = .fillProportionally
        pinBackground(otherBackgroundView, to: aggregateStack)

        // If I comment out the following, there are no constraint conflicts
        aggregateStack.spacing = 5

        field1.translatesAutoresizingMaskIntoConstraints = false
        field2.translatesAutoresizingMaskIntoConstraints = false

        aggregateStack.addArrangedSubview(field1)
        aggregateStack.addArrangedSubview(field2)
        rootStack.addArrangedSubview(aggregateStack)

        view.addSubview(rootStack)
        rootStack.translatesAutoresizingMaskIntoConstraints = false

        /**
         * pin the root stackview to the edges of the view controller, just so we can see
         * it's behavior
         */
        NSLayoutConstraint.activate([
            rootStack.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant:16),
            rootStack.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant:-16),
            rootStack.topAnchor.constraint(equalTo: view.topAnchor, constant:32),
            rootStack.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant:-32),
            ])
    }

    /**
     * Inserts a UIView into the UIStackView's hierarchy, but not as part of the arranged subviews
     * see https://useyourloaf.com/blog/stack-view-background-color/
     */
    private func pinBackground(_ view: UIView, to stackView: UIStackView) {
        view.translatesAutoresizingMaskIntoConstraints = false
        stackView.insertSubview(view, at: 0)
        NSLayoutConstraint.activate([
            view.leadingAnchor.constraint(equalTo: stackView.leadingAnchor),
            view.trailingAnchor.constraint(equalTo: stackView.trailingAnchor),
            view.topAnchor.constraint(equalTo: stackView.topAnchor),
            view.bottomAnchor.constraint(equalTo: stackView.bottomAnchor)
            ])
    }

    override public func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {

        super.traitCollectionDidChange(previousTraitCollection)

        switch traitCollection.verticalSizeClass {
        case .regular:
            aggregateStack.axis = .vertical
        case .compact:
            aggregateStack.axis = .horizontal
        case .unspecified:
            print("Unexpected")
        }
    }
}

情侣笔记...

  • "nested" 堆栈视图存在固有问题,导致约束冲突。这可以通过将受影响元素的优先级设置为 999(而不是默认的 1000)来避免。
  • 您的布局变得有点复杂...标签 "attached" 到文本字段;元素需要在两个 "lines" 纵向方向上或一个 "line" 横向方向上; "multi-element line" 的一个元素具有不同的高度(步进器);等等。
  • 要使 "field2" 和 "field3" 大小相等,您需要将它们的宽度限制为相等,即使它们不是同一子视图的子视图。这是完全有效的,只要它们是同一视图层次结构的后代。
  • Stackviews 很棒 --- 除非它们不是。我几乎会建议只使用约束。您需要添加更多约束,但它可能避免一些堆栈视图问题。

但是,这里有一个示例可以帮助您上手。

我添加了一个名为 LabeledFieldStackViewUIStackView 子类...它在堆栈视图中设置了 Label-above-Field。比将它混合在所有其他布局代码中要干净一些。

class LabeledFieldStackView: UIStackView {

    var theLabel: UILabel = {
        let v = UILabel()
        v.translatesAutoresizingMaskIntoConstraints = false
        return v
    }()

    var theField: UITextField = {
        let v = UITextField()
        v.translatesAutoresizingMaskIntoConstraints = false
        v.borderStyle = .roundedRect
        return v
    }()

    convenience init(with labelText: String, fieldText: String, verticalGap: CGFloat) {

        self.init()

        axis = .vertical
        alignment = .fill
        distribution = .fill
        spacing = 2

        addArrangedSubview(theLabel)
        addArrangedSubview(theField)

        theLabel.text = labelText
        theField.text = fieldText

        self.translatesAutoresizingMaskIntoConstraints = false

    }

}

class LargentViewController: UIViewController {

    var rootStack: UIStackView!

    var fieldStackView1: LabeledFieldStackView!
    var fieldStackView2: LabeledFieldStackView!
    var fieldStackView3: LabeledFieldStackView!
    var fieldStackView4: LabeledFieldStackView!

    var stepper: UIStepper!

    var fieldAndStepperStack: UIStackView!

    var twoLineStack: UIStackView!

    var fieldAndStepperStackWidthConstraint: NSLayoutConstraint!

    // horizontal gap between elements on the same "line"
    var horizontalSpacing: CGFloat!

    // vertical gap between "lines"
    var verticalSpacing: CGFloat!

    // vertical gap between labels above text fields
    var labelToFieldSpacing: CGFloat!

    override func viewDidLoad() {

        super.viewDidLoad()

        view.backgroundColor = UIColor(white: 0.9, alpha: 1.0)

        horizontalSpacing = CGFloat(2)
        verticalSpacing = CGFloat(8)
        labelToFieldSpacing = CGFloat(2)

        createIntializeViews()
        createInitializeStacks()
        fillStacks()

    }

    private func createIntializeViews() {

        fieldStackView1 = LabeledFieldStackView(with: "label 1", fieldText: "field 1", verticalGap: labelToFieldSpacing)
        fieldStackView2 = LabeledFieldStackView(with: "label 2", fieldText: "field 2", verticalGap: labelToFieldSpacing)
        fieldStackView3 = LabeledFieldStackView(with: "label 3", fieldText: "field 3", verticalGap: labelToFieldSpacing)
        fieldStackView4 = LabeledFieldStackView(with: "label 4", fieldText: "field 4", verticalGap: labelToFieldSpacing)

        stepper = UIStepper()

    }

    private func createInitializeStacks() {

        rootStack = UIStackView()
        fieldAndStepperStack = UIStackView()
        twoLineStack = UIStackView()

        [rootStack, fieldAndStepperStack, twoLineStack].forEach {
            [=10=]?.translatesAutoresizingMaskIntoConstraints = false
        }

        // rootStack has spacing of horizontalSpacing (inter-line vertical spacing)
        rootStack.axis = .vertical
        rootStack.alignment = .fill
        rootStack.distribution = .fill
        rootStack.spacing = verticalSpacing

        // fieldAndStepperStack has spacing of horizontalSpacing (space between field and stepper)
        // and .alignment of .bottom (so stepper aligns vertically with field)
        fieldAndStepperStack.axis = .horizontal
        fieldAndStepperStack.alignment = .bottom
        fieldAndStepperStack.distribution = .fill
        fieldAndStepperStack.spacing = horizontalSpacing

        // twoLineStack has inter-line vertical spacing of
        //   verticalSpacing in portrait orientation
        // for landscape orientation, the two "lines" will be changed to one "line"
        //  and the spacing will be changed to horizontalSpacing
        twoLineStack.axis = .vertical
        twoLineStack.alignment = .leading
        twoLineStack.distribution = .fill
        twoLineStack.spacing = verticalSpacing

    }

    private func fillStacks() {

        self.view.addSubview(rootStack)

        // constrain rootStack Top, Leading, Trailing = 20
        // no height or bottom constraint
        NSLayoutConstraint.activate([
            rootStack.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 20.0),
            rootStack.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 20.0),
            rootStack.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -20.0),
            ])

        rootStack.addArrangedSubview(fieldStackView1)

        fieldAndStepperStack.addArrangedSubview(fieldStackView2)
        fieldAndStepperStack.addArrangedSubview(stepper)

        twoLineStack.addArrangedSubview(fieldAndStepperStack)
        twoLineStack.addArrangedSubview(fieldStackView3)

        rootStack.addArrangedSubview(twoLineStack)

        // fieldAndStepperStack needs width constrained to its superview (the twoLineStack) when
        //  in portrait orientation
        // setting the priority to 999 prevents "nested stackView" constraint breaks
        fieldAndStepperStackWidthConstraint = fieldAndStepperStack.widthAnchor.constraint(equalTo: twoLineStack.widthAnchor, multiplier: 1.0)
        fieldAndStepperStackWidthConstraint.priority = UILayoutPriority(rawValue: 999)

        // constrain fieldView3 width to fieldView2 width to keep them the same size
        NSLayoutConstraint.activate([
            fieldStackView3.widthAnchor.constraint(equalTo: fieldStackView2.widthAnchor, multiplier: 1.0)
            ])

        rootStack.addArrangedSubview(fieldStackView4)

    }

    override public func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {

        super.traitCollectionDidChange(previousTraitCollection)

        if traitCollection.verticalSizeClass == .regular {
            fieldAndStepperStackWidthConstraint.isActive = true
            twoLineStack.axis = .vertical
            twoLineStack.spacing = verticalSpacing
        } else if traitCollection.verticalSizeClass == .compact {
            fieldAndStepperStackWidthConstraint.isActive = false
            twoLineStack.axis = .horizontal
            twoLineStack.spacing = horizontalSpacing
        } else {
            print("Unexpected")
        }
    }

}

结果: