web-dev-qa-db-ja.com

組み込みコンテンツサイズのみのUIStackView比例レイアウト

UIStackViewで配置されたサブビューのレイアウトに問題があり、何が起こっているのかを誰かが理解するのを手伝ってくれるかどうか疑問に思っていました。

したがって、UIStackViewには、ある程度の間隔(たとえば、1ですが、これは問題ではありません)と.fillProportionally分散があります。私は1x1のintrinsicContentSizeのみで配置されたサブビューを追加しています(何でもかまいませんが、正方形のビューだけです)。stackView内で比例的に引き伸ばす必要があります。

問題は、実際のフレームなしで、固有のサイズのみでビューを追加すると、この間違ったレイアウトになることです。

wrong layout

それ以外の場合、同じサイズのフレームでビューを追加すると、すべてが期待どおりに機能します。

correct layout

しかし、私はビューのフレームをまったく設定したくないのです。

これはハグと耐圧縮性の優先順位がすべてだと確信していますが、正しい答えが何であるかを理解することはできません。

Playgroundの例を次に示します。

import UIKit
import PlaygroundSupport

class LView: UIView {

    // If comment this and leave only intrinsicContentSize - result is wrong
    convenience init() {
        self.init(frame: CGRect(x: 0, y: 0, width: 1, height: 1))
    }

    // If comment this and leave only convenience init(), then everything works as expected
    public override var intrinsicContentSize: CGSize {
        return CGSize(width: 1, height: 1)
    }
}

let container = UIView(frame: CGRect(x: 0, y: 0, width: 300, height: 300))
container.backgroundColor = UIColor.white

let sv = UIStackView()
container.addSubview(sv)


sv.leftAnchor.constraint(equalTo: container.leftAnchor).isActive = true
sv.rightAnchor.constraint(equalTo: container.rightAnchor).isActive = true
sv.topAnchor.constraint(equalTo: container.topAnchor).isActive = true
sv.bottomAnchor.constraint(equalTo: container.bottomAnchor).isActive = true
sv.translatesAutoresizingMaskIntoConstraints = false
sv.spacing = 1
sv.distribution = .fillProportionally

// Adding arranged subviews to stackView, 24 elements with intrinsic size 1x1
for i in 0..<24 {
    let a = LView()
    a.backgroundColor = (i%2 == 0 ? UIColor.red : UIColor.blue)
    sv.addArrangedSubview(a)
}
sv.layoutIfNeeded()

PlaygroundPage.current.liveView = container
13
Arthur Grishin

これは明らかにUIStackViewの実装のバグです(つまり、システムのバグです)。

DonMagは、彼のコメントですでに正しい方向を指すヒントを与えています:

スタックビューのspacingを0に設定すると、すべてが期待どおりに機能します。ただし、他の値に設定すると、レイアウトが壊れます。


理由は次のとおりです。

ℹ️簡単にするために、スタックビューには

  • a横軸および
  • 10個の配置されたサブビュー

.fillProportionallyディストリビューションを使用してUIStackViewは、次のようにシステム制約を作成します。

  • 配置されたサブビューごとに、等しい幅制約(UISV-fill-proportionally)を追加します。 multiplierを使用してスタックビュー自体に:

    arrangedSubview[i].width = multiplier[i] * stackView.width
    

    スタックビューにn配置されたサブビューがある場合、これらの制約のnを取得します。それらをproportionalConstraint[i]と呼びましょう(ここで、iarrangedSubviews配列内のそれぞれのビューの位置を示します)。

  • これらの制約は不要です(つまり、優先度は1000ではありません)。代わりに、arrangedSubviews配列の最初の要素の制約には999の優先度が割り当てられ、2番目の要素には998の優先度が割り当てられます。

    proportionalConstraint[0].priority = 999 
    proportionalConstraint[1].priority = 998 
    proportionalConstraint[2].priority = 997 
    proportionalConstraint[3].priority = 996
    ...         
    proportionalConstraint[n–1].priority = 1000 – n
    

    これは、必要な制約が常にこれらの比例制約に勝つことを意味します!

  • 配置されたサブビューを(おそらく間隔を空けて)接続するために、システムはUISV-spacingと呼ばれるn–1制約も作成します。

    arrangedSubview[i].trailing + spacing = arrangedSubview[i+1].leading
    

    これらの制約は必須です(つまり、優先度= 1000)。

  • (システムは他のいくつかの制約も作成します(たとえば、垂直軸や、最初と最後に配置されたサブビューをスタックビューのエッジに固定するため)が、それらは何であるかを理解するのに関係がないため、ここでは詳しく説明しませんうまくいかない。)

Appleのドキュメント.fillProportionallyディストリビューションの状態:

スタックビューが配置されたビューのサイズを変更して、スタックビューの軸に沿って使用可能なスペースを埋めるレイアウト。ビューは、スタックビューの軸に沿った固有のコンテンツサイズに基づいて比例してサイズ変更されます。

したがって、これによれば、multipliersのproportionalConstraintは次のように計算する必要がありますspacing = 0の場合:

  • totalIntrinsicWidth = ∑intrinsicWidth[i]
  • multiplier[i] = intrinsicWidth[i]/totalIntrinsicWidth

配置された10個のサブビューがすべて同じ固有の幅を持っている場合、これは期待どおりに機能します。

multiplier[i] = 0.1

すべてのproportionalConstraintsに対して。ただし、間隔をゼロ以外の値に変更するとすぐに、間隔の幅を考慮する必要があるため、multiplierの計算ははるかに複雑になります。私は数学を行い、multiplier[i]の式は次のとおりです。

How the stack view should compute the multiplier


例:

次のように構成されたスタックビューの場合:

  • stackView.width = 400
  • stackView.spacing = 2

上記の式は次のようになります。

multiplier[i] = 0.0955

あなたはそれを合計することによってこれが正しいことを証明することができます:

(10 * width) + (9 * spacing)
    = (10 * multiplier * stackViewWidth) + (9 * spacing)
    = (10 * 0.0955 * 400) + (9 * 2)
    = (0.955 * 400) + 18
    = 382 + 18
    = 400
    = stackViewWidth

ただし、システムは別の値を割り当てます。

multiplier[i] = 0.0917431

合計すると、

(10 * width) + (9 * spacing)
    = (10 * 0.0917431 * 400) + (9 * 2)
    = 384,97
    < stackViewWidth

明らかに、この値は間違っています。

結果として、システムは制約を破らなければなりません。そしてもちろん、最後に配置されたサブビューアイテムのproportionalConstraintである最も低い優先度で制約を破ります。

これが、スクリーンショットで最後に配置されたサブビューが引き伸ばされる理由です。

さまざまな間隔とスタックビューの幅を試してみると、あらゆる種類の奇妙なレイアウトになってしまいます。ただし、これらすべてに共通する点が1つあります。間隔は常に優先されます。(間隔を30や40などの大きな値に設定すると、残りのスペースは必要な間隔で完全に占有されているため、最初の2つまたは3つの配置されたサブビューのみが表示されます。)


要約すると:

.fillProportionallyディストリビューションは、spacing = 0でのみ正しく機能します。

他の間隔の場合、システムは誤った乗数で制約を作成します。

これはレイアウトを壊します

  • 乗数が本来よりも小さい場合は、配置されたサブビューのいずれか(最後)をストレッチする必要があります
  • 乗数が本来よりも大きい場合は、複数の配置されたサブビューを圧縮する必要があります。

これを回避する唯一の方法は、ビュー間の間隔として必要な固定幅の制約を使用して、プレーンなUIViewsを「誤用」することです。 (通常、UILayoutGuidesはこの目的で導入されましたが、スタックビューにレイアウトガイドを追加できないため、これらを使用することもできません。)

このバグが原因で、これを行うためのクリーンなソリューションがないのではないかと思います。

24
Mischa

少なくともXcode9.2の時点では、初期化子と固有のコンテンツサイズが両方コメントアウトされていれば、提供されるプレイグラウンドは意図したとおりに機能します。その場合、間隔が0より大きい場合でも、比例充填は期待どおりに機能します。

これは間隔= 5の例です

enter image description here

配置されたサブビューには固有のコンテンツサイズがなく、StackViewは指定された軸を比例的に埋めるように幅を決定するため、これは理にかなっているようです。

初期化子のみが有効になっている場合(固有のコンテンツサイズのオーバーライドは有効になっていない場合)、これが表示されます。これはプレイグラウンドのコメントと一致しないため、質問が投稿されてからこの動作が変更されたに違いありません。

enter image description here

自動レイアウトを使用する場合、フレームを手動で設定することは無視する必要があるように思われるため、その動作を理解していません。

固有のコンテンツサイズのオーバーライドのみが有効になっている場合(初期化子は有効になっていない場合)、この投稿の元となった問題のある画像が表示されます(ここでは間隔= 5): enter image description here

基本的に、指定された固有のコンテンツサイズのために、ビューの幅を1ポイントにする必要があるため、デザインが競合し、実現できません。ここの合計スペースは

24 views * 1 point/view + 23 spaces * 5 points/space = 139 < 300 = sv.bounds.width

mischaが指摘したように、優先度が最も低いため、最後に配置されたビューの制約が解除されます。

ピクセルごとの検査では、上記の最初の23ビューは1ピクセルよりも幅が広いですが、実際には最後のビューを除いて2または3ピクセルであるため、計算が完全に一致しません。理由はわかりません。おそらく切り上げます。 10進数の?

参考までに、これは、幅5の固有のコンテンツサイズを使用した場合のように見えますが、それでも制約を満たしていません。

enter image description here

2
atineoSE