39

我有一个复杂的视图层次结构,内置在 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 下降,但仍然无法解决问题。

4

7 回答 7

44

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

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

  1. 将间距更改为 0,但您需要记住之前的间距值。
  2. Call innerStackView.removeFromSuperview(),但是你需要记住在哪里插入堆栈视图。
  3. 将堆栈视图包装在具有至少一个 999 约束的 UIView 中。例如顶部@1000、前导@1000、尾随@1000、底部@999​​。

第三种选择在我看来是最好的。有关此问题、发生原因、不同解决方案以及如何实施解决方案 3 的更多信息,请参阅我对类似问题的回答

于 2016-07-07T04:57:39.567 回答
27

所以,你有这个:

破碎的动画

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

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.

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

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

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

这是上面损坏版本的场景大纲:

破碎的轮廓

要解决此问题,请选择内部堆栈视图。从菜单栏中,选择编辑器 > 嵌入 > 视图:

嵌入视图

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

删除宽度约束

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

创建约束

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

拥抱优先事项

在这一点上,我们实际上还没有解决不可满足的约束问题。为此,请找到您刚刚创建的底部约束并将其优先级设置为低于要求。让我们将其更改为 800:

更改底部约束优先级

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

换出口

Now, when you toggle the wrapper view's hidden property, the stack view will appear to collapse, with no unsatisfiable constraint warnings. It looks virtually identical, so I won't bother posting another GIF of the app running.

You can find my test project in this github repository.

于 2017-07-02T21:19:49.620 回答
19

我在 UISV 隐藏中遇到了类似的问题。对我来说,解决方案是将我自己的约束的优先级从Required (1000) 减少到更少。添加 UISV 隐藏约束时,它们具有优先权,并且约束不再冲突。

于 2015-10-15T07:36:04.080 回答
16

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

spacing在隐藏之前成功地将嵌套堆栈视图的属性设置为 0,并在使其再次可见后恢复到正确的值。

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

我的项目只有一层嵌套,所以我不确定这是否会导致 FPS 问题。只要您不对间距的变化进行动画处理,我认为它不会产生太大的影响。

于 2015-10-14T14:46:00.867 回答
2

Another approach

Try to avoid nested UIStackViews. I love them and build almost everything with them. But as I recognized that they secretly add constraints I try to only use them at the highest level and non-nested where possible. This way I can specify the 2nd highest priority .defaultHighto the spacing constraint which resolves my warnings.

This priority is just enough to prevent most layout issues.

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

于 2017-11-02T14:57:40.570 回答
0

这是使用 SnapKit 约束编写为 Swift 3 类的 Senseful 建议 #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;
        }
    }
}
于 2016-12-24T07:01:09.320 回答
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
    }
于 2020-08-29T14:14:49.837 回答