web-dev-qa-db-ja.com

ネストされたUIStackViewsの壊れた制約

ネストされたUIStackViewsを持つInterface Builderで構築された複雑なビュー階層があります。内部スタックビューの一部を非表示にするたびに、「不満足な制約」通知を受け取ります。私はこれを追跡しました:

(
    "<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の定数を取得しますが、内部スタックビューの間隔制約と衝突するようです:ラベルとボタンの間に8ポイントが必要であり、非表示の制約と一致しないため制約がクラッシュします。

これを回避する方法はありますか?非表示のスタックビューのすべての内部StackViewsを再帰的に非表示にしようとしましたが、その結果、コンテンツが画面外に浮かび、問題を修正しなくても重大なFPSドロップを引き起こす奇妙なアニメーションが発生します。

26
Alex Popov

理想的には、UISV-spacingより低い値に制約しますが、それを行う方法はないようです。 :)

ネストされたスタックビューのspacingプロパティを非表示にする前に0に設定し、再び表示した後に適切な値に復元することに成功しています。

ネストされたスタックビューでこれを再帰的に実行すると動作すると思います。 spacingプロパティの元の値を辞書に保存し、後で復元することができます。

私のプロジェクトには単一レベルのネストしかありません。そのため、FPSの問題が発生するかどうかはわかりません。間隔の変更をアニメートしない限り、あまり大きなヒットを作成するとは思わない。

16
Cory Juhlin

これは、ネストされたスタックビューの非表示に関する既知の問題です。

この問題には基本的に3つの解決策があります。

  1. 間隔を0に変更しますが、前の間隔値を覚えておく必要があります。
  2. innerStackView.removeFromSuperview()を呼び出しますが、スタックビューを挿入する場所を覚えておく必要があります。
  3. 少なくとも1つの999制約を使用して、スタックビューをUIViewにラップします。例えば。 top @ 1000、leading @ 1000、trailing @ 1000、bottom @ 999。

私の意見では、3番目のオプションが最適です。この問題、それが発生する理由、さまざまなソリューション、およびソリューション3の実装方法の詳細については、 同様の質問に対する私の答え を参照してください。

23
Senseful

UISV非表示でも同様の問題が発生しました。私にとっての解決策は、自分の制約の優先順位をRequired(1000)からそれよりも低い値に下げることでした。 UISV非表示の制約が追加されると、それらが優先され、制約が衝突しなくなります。

18
Jaanus

だから、あなたはこれを持っています:

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.

指摘したように、問題は、外側のスタックビューが内側のスタックビューに高さ= 0の制約を適用することです。これは、独自のサブビュー間で内部スタックビューによって適用される8ポイントパディング制約と競合します。両方の制約を同時に満たすことはできません。

外側のスタックビューは、この高さ= 0の制約を使用します。アニメーション化するときは、最初に縮小せずに内側のビューを非表示にするよりも見た目が良いと思います。

これには簡単な修正があります:プレーンUIViewで内部スタックビューをラップし、そのラッパーを非表示にします。デモします。

上記の壊れたバージョンのシーンの概要は次のとおりです。

broken outline

問題を修正するには、内部スタックビューを選択します。メニューバーから、[エディター]> [埋め込み]> [表示]を選択します。

embed in view

Interface Builderは、これを行ったときにラッパービューに幅の制約を作成したため、その幅の制約を削除します。

delete width constraint

次に、ラッパーの4つのエッジすべてと内部スタックビューの間に制約を作成します。

create constraints

この時点で、実行時のレイアウトは実際には正しいのですが、Interface Builderは間違って描画します。内側のスタックの子の垂直方向のハグの優先順位を高く設定することで修正できます。 800に設定します。

hugging priorities

現時点では、満足できない制約の問題を修正していません。これを行うには、作成したばかりの下部の制約を見つけて、その優先度を必要未満に設定します。 800に変更しましょう。

change bottom constraint priority

最後に、hiddenプロパティを変更していたため、View Controllerにアウトレットが内部スタックビューに接続されていたと思われます。そのアウトレットを変更して、内部スタックビューではなくラッパービューに接続します。コンセントのタイプがUIStackViewの場合、UIViewに変更する必要があります。私はすでにUIViewタイプでしたので、ストーリーボードで再接続しました。

change outlet

これで、ラッパービューのhiddenプロパティを切り替えると、スタックビューが折りたたまれたように見え、満たされない制約の警告はありません。ほぼ同じように見えるため、実行中のアプリの別のGIFを投稿する必要はありません。

私のテストプロジェクトを見つけることができます このgithubリポジトリ内

17
rob mayoff

別のアプローチ

ネストされたUIStackViewsを避けるようにしてください。私は彼らを愛し、彼らとほとんどすべてを構築します。しかし、それらが密かに制約を追加することを認識したので、可能な限りネストせずに最高レベルでのみ使用するようにします。このようにして、2番目に高い優先度を指定できます.defaultHighto警告を解決する間隔制約。

この優先順位は、ほとんどのレイアウトの問題を防ぐのに十分です。

もちろん、さらにいくつかの制約を指定する必要がありますが、この方法で完全に制御し、ビューレイアウトを明示的にすることができます。

2
blackjacx

Swift 3 SnapKit制約を使用したクラス。

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;
        }
    }
}
0
Antti