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

Posted

技术标签:

【中文标题】当 trait 集合发生变化时,就会出现约束冲突,就好像 stackview 轴没有改变一样【英文标题】:When trait collection changes, constraint conflicts arise as though the stackview axis didn't change 【发布时间】:2019-01-28 18:08:05 【问题描述】:

我有一个带有两个控件的 stackview。

当 UI 不受垂直约束时: Vertical1

当UI垂直受限时:Horizontal1

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

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

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

【问题讨论】:

为什么要为两个文本字段添加前导/尾随约束?很确定这是导致约束问题的原因。 @DonMag 在完整的实现中,我有几行不同的控件。它是用户使用文本字段、步进器和分段控件填写的表单。当没有垂直约束时,每行的文本字段控件沿它们的后沿对齐。 正在查看您的“更多形式”图像......它看起来像是纵向方向?如果是这样,您希望它在横向中看起来如何?或者,是横向,并且您希望步进器在纵向中移动到 TextField-2 下方? 在横向中,TextField 2、Stepper 和 TextField 3 按此顺序位于同一行。我用横向的图片更新了原始帖子。 这是你想要得到的吗? imgur.com/a/L6KB0Tl 【参考方案1】:

情侣笔记...

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

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

我添加了一个名为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 
            $0?.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")
        
    


结果:

【讨论】:

在阅读了许多文章和反复试验之后,您发布的笔记简洁地汇集了我看到或怀疑的关键问题。你的例子效果很好。谢谢。【参考方案2】:

UIView 添加到 UIStackView 时,stackView 将根据分配给 stackView 的属性(axisalignmentdistributionspacing)为该视图分配约束。正如@DonMag 所提到的,您正在为aggregateStack 视图中的textField 添加一个约束。 aggregateStack 将根据其属性添加自己的约束。通过删除该约束和激活/停用代码,约束冲突消失。

我使用您的代码创建了一个小示例,并向 stackViews 添加了一些背景视图,以便您可以更轻松地查看更改各种属性时发生的情况。只是为了说明,我将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")
        
    

【讨论】:

我有后沿对齐控件,因为当我有一个每行有多个控件的表单时,我不知道有其他方法可以对齐边缘。 让我吃惊的是,当我第一次加载视图时,怎么没有约束冲突?然后我旋转设备并且没有约束冲突。但是将设备返回到初始的非垂直约束方向则显示约束冲突。在我看来,stackview 和 nonstackview 约束的应用变得复杂。如果不考虑我将stackview轴更改为垂直,就好像系统的约束检查代码一样。 wtfautolayout 输出表明旋转后存在挥之不去的水平约束。 可以肯定的是,我几乎总是假设我的某个假设或理解是错误的。我确实相信有某种方式我不会考虑需要更改的堆栈视图。是我应该使用堆栈视图或视图约束而不是混合两者吗? 我喜欢UIStackView,但是当你开始嵌套它们时它们会变得很棘手。正如我所提到的,它们添加了自己的约束,这些约束可能与您添加的任何约束相冲突。您可能需要向视图添加约束以设置其大小,例如,如果 UIView 没有内在内容大小(如 UITextFieldsUILabel 做),UIStackView 使用它来应用诸如fillEqually。我尝试让UIStackView 完成大部分布局工作。 关于“当你开始嵌套它们时会变得很棘手”。我同意。看来我的表格很简单。然而,嵌套堆栈视图并没有约束冲突已被证明有些劳动密集型。我希望找到一些我可以应用的模式,以便更有效地生成表单。

以上是关于当 trait 集合发生变化时,就会出现约束冲突,就好像 stackview 轴没有改变一样的主要内容,如果未能解决你的问题,请参考以下文章

当设备方向发生变化时,UIViewController 不会更新视图的约束

当 UINavigation 的 Translucent 设置为 false 时,视图约束发生变化

当集合视图大小发生变化时,我需要帮助为 UICollectionView 单元格大小设置动画

发生约束冲突时如何调用触发器?

为啥当键盘出现时 UICollectionView 偏移量会发生变化

当 UIAlertView 出现时 TTTAttributedLabel 链接字体发生变化