嵌套的UIStack查看破坏的约束

时间:2015-10-12 04:09:14

标签: ios cocoa-touch autolayout interface-builder uistackview

我有一个复杂的视图层次结构,内置在Interface Builder中,带有嵌套的UIStackViews。我得到了不满足的约束"每次我隐藏一些内部堆栈视图时都会注意到。我已将其追踪到此:

(
    "<NSLayoutConstraint:0x1396632d0 'UISV-canvas-connection' UIStackView:0x1392c5020.top == UILabel:0x13960cd30'Also available on iBooks'.top>",
    "<NSLayoutConstraint:0x139663470 'UISV-canvas-connection' V:[UIButton:0x139554f80]-(0)-|   (Names: '|':UIStackView:0x1392c5020 )>",
    "<NSLayoutConstraint:0x139552350 'UISV-hiding' V:[UIStackView:0x1392c5020(0)]>",
    "<NSLayoutConstraint:0x139663890 'UISV-spacing' V:[UILabel:0x13960cd30'Also available on iBooks']-(8)-[UIButton:0x139554f80]>"
)

具体来说,UISV-spacing约束:当隐藏UIStackView时,它的高约束变为0常量,但这似乎与内部stackview的间距约束冲突:它需要我的Label和Button之间的8个点,这与隐藏约束不可调和,因此约束崩溃。

有解决方法吗?我已经尝试递归隐藏隐藏堆栈视图的所有内部StackView,但这导致奇怪的动画,其中内容浮出屏幕,并导致严重的FPS丢弃启动,同时仍然没有解决问题。

7 个答案:

答案 0 :(得分:20)

这是隐藏嵌套堆栈视图的已知问题。

这个问题基本上有3个解决方案:

  1. 将间距更改为0,但是您需要记住之前的间距值。
  2. 调用innerStackView.removeFromSuperview(),但是您需要记住插入堆栈视图的位置。
  3. 在具有至少一个999约束的UIView中包装堆栈视图。例如。 top @ 1000,领先@ 1000,尾随@ 1000,底部@ 999。
  4. 第三种选择在我看来是最好的。有关此问题的更多信息,原因,不同的解决方案以及如何实施解决方案3,请参阅my answer to a similar question

答案 1 :(得分:16)

我遇到了类似UISV隐藏的问题。对我来说,解决方案是将我自己的约束优先级从Required(1000)减少到小于此值。当添加UISV隐藏约束时,它们优先,约束不再发生冲突。

答案 2 :(得分:16)

所以,你有这个:

broken animation

问题是,当你第一次折叠内部堆栈时,会出现自动布局错误:

2017-07-02 15:40:02.377297-0500 nestedStackViews[17331:1727436] [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:0x62800008ce90 'UISV-canvas-connection' UIStackView:0x7fa57a70fce0.top == UILabel:0x7fa57a70ffb0'Top Label of Inner Stack'.top   (active)>",
    "<NSLayoutConstraint:0x62800008cf30 'UISV-canvas-connection' V:[UILabel:0x7fa57d30def0'Bottom Label of Inner Sta...']-(0)-|   (active, names: '|':UIStackView:0x7fa57a70fce0 )>",
    "<NSLayoutConstraint:0x62000008bc70 'UISV-hiding' UIStackView:0x7fa57a70fce0.height == 0   (active)>",
    "<NSLayoutConstraint:0x62800008cf80 'UISV-spacing' V:[UILabel:0x7fa57a70ffb0'Top Label of Inner Stack']-(8)-[UILabel:0x7fa57d30def0'Bottom Label of Inner Sta...']   (active)>"
)

Will attempt to recover by breaking constraint 
<NSLayoutConstraint:0x62800008cf80 'UISV-spacing' V:[UILabel:0x7fa57a70ffb0'Top Label of Inner Stack']-(8)-[UILabel:0x7fa57d30def0'Bottom Label of Inner Sta...']   (active)>

Make a symbolic breakpoint at UIViewAlertForUnsatisfiableConstraints to catch this in the debugger.
The methods in the UIConstraintBasedLayoutDebugging category on UIView listed in <UIKit/UIView.h> may also be helpful.

正如您所指出的,问题是外部堆栈视图将height = 0约束应用于内部堆栈视图。这与内部堆栈视图在其自己的子视图之间应用的8点填充约束冲突。两种约束都不能同时满足。

外部堆栈视图使用此height = 0约束,我相信,因为它在动画时看起来比仅让内部视图隐藏而不先收缩更好。

有一个简单的解决方法:将内部堆栈视图包装在普通的UIView中,并隐藏该包装器。我会演示。

以下是上面破碎版本的场景轮廓:

broken outline

要解决此问题,请选择内部堆栈视图。从菜单栏中选择Editor&gt;嵌入&gt;视图:

embed in view

当我这样做时,Interface Builder在包装器视图上创建了一个宽度约束,因此删除该宽度约束:

delete width constraint

接下来,在包装器的所有四个边和内部堆栈视图之间创建约束:

create constraints

此时,布局在运行时实际上是正确的,但Interface Builder会错误地绘制它。您可以通过将内部堆栈的子项的垂直拥抱优先级设置得更高来修复它。我把它们设置为800:

hugging priorities

此时我们还没有解决不可满足的约束问题。为此,请找到刚刚创建的底部约束,并将其优先级设置为低于要求。我们把它改成800:

change bottom constraint priority

最后,您可能在视图控制器中有一个连接到内部堆栈视图的插座,因为您正在更改其hidden属性。将该出口更改为连接到包装器视图而不是内部堆栈视图。如果您的商店类型为UIStackView,则需要将其更改为UIView。我已经是UIView类型,所以我只是在故事板中重新连接它:

change outlet

现在,当您切换包装器视图的hidden属性时,堆栈视图将显示为折叠,没有不可满足的约束警告。它看起来几乎完全相同,所以我不打算发布另一个正在运行的应用程序的GIF。

您可以找到我的测试项目in this github repository

答案 3 :(得分:15)

理想情况下,我们可以将UISV-spacing约束的优先级设置为较低的值,但似乎没有任何方法可以做到这一点。 :)

我成功将嵌套堆栈视图的spacing属性设置为0,然后再隐藏,并在再次显示之后恢复到正确的值。

我认为在嵌套堆栈视图上递归执行此操作会起作用。您可以将spacing属性的原始值存储在字典中,稍后再恢复。

我的项目只有一个嵌套级别,所以我不确定这是否会导致FPS问题。只要你没有设置间距变化的动画,我认为它不会造成太多的打击。

答案 4 :(得分:1)

另一种方法

尽量避免嵌套的UIStackViews。我爱他们并用他们建造几乎所有东西。但是当我认识到他们秘密添加约束时,我尝试仅在最高级别使用它们并且尽可能不嵌套。这样我就可以为间距约束指定第二高优先级.defaultHigh来解决我的警告。

此优先级足以防止大多数布局问题。

当然,您需要指定更多约束,但这样您就可以完全控制它们并使视图布局显式化。

答案 5 :(得分:0)

这里使用SnapKit约束编写了作为Swift 3类编写的Senseful#3建议#3。我也试过覆盖这些属性,但是没有警告就没有工作,所以我会坚持使用UIStackView包装:

class NestableStackView: UIView {
    private var actualStackView = UIStackView()

    override init(frame: CGRect) {
        super.init(frame: frame);
        addSubview(actualStackView);
        actualStackView.snp.makeConstraints { (make) in
            // Lower edges priority to allow hiding when spacing > 0
            make.edges.equalToSuperview().priority(999);
        }
    }

    convenience init() {
        self.init(frame: CGRect.zero);
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    func addArrangedSubview(_ view: UIView) {
        actualStackView.addArrangedSubview(view);
    }

    func removeArrangedSubview(_ view: UIView) {
        actualStackView.removeArrangedSubview(view);
    }

    var axis: UILayoutConstraintAxis {
        get {
            return actualStackView.axis;
        }
        set {
            actualStackView.axis = newValue;
        }
    }

    open var distribution: UIStackViewDistribution {
        get {
            return actualStackView.distribution;
        }
        set {
            actualStackView.distribution = newValue;
        }
    }

    var alignment: UIStackViewAlignment {
        get {
            return actualStackView.alignment;
        }
        set {
            actualStackView.alignment = newValue;
        }
    }

    var spacing: CGFloat {
        get {
            return actualStackView.spacing;
        }
        set {
            actualStackView.spacing = newValue;
        }
    }
}

答案 6 :(得分:0)

在我的情况下,我在导航栏按钮上添加了宽度和高度约束,按照上面的建议,我只给约束添加了较低的优先级。

open func customizeNavigationBarBackButton() {
        let _selector = #selector(UIViewController._backButtonPressed(_:))
        let backButtonView = UIButton(type: .custom)
        backButtonView.setImage(UIImage(named: "icon_back"), for: .normal)
        backButtonView.imageEdgeInsets = UIEdgeInsets.init(top: 0, left: -30, bottom: 0, right: 0)
        backButtonView.snp.makeConstraints { $0.width.height.equalTo(44).priority(900) }
        backButtonView.addTarget(self, action: _selector, for: .touchUpInside)

        let backButton = UIBarButtonItem(customView: backButtonView)
        self.navigationItem.leftBarButtonItem = backButton
    }