UIStackViewで配置されたサブビューのレイアウトに問題があり、何が起こっているのかを誰かが理解するのを手伝ってくれるかどうか疑問に思っていました。
したがって、UIStackViewには、ある程度の間隔(たとえば、1ですが、これは問題ではありません)と.fillProportionally分散があります。私は1x1のintrinsicContentSizeのみで配置されたサブビューを追加しています(何でもかまいませんが、正方形のビューだけです)。stackView内で比例的に引き伸ばす必要があります。
問題は、実際のフレームなしで、固有のサイズのみでビューを追加すると、この間違ったレイアウトになることです。
それ以外の場合、同じサイズのフレームでビューを追加すると、すべてが期待どおりに機能します。
しかし、私はビューのフレームをまったく設定したくないのです。
これはハグと耐圧縮性の優先順位がすべてだと確信していますが、正しい答えが何であるかを理解することはできません。
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
これは明らかにUIStackView
の実装のバグです(つまり、システムのバグです)。
DonMagは、彼のコメントですでに正しい方向を指すヒントを与えています:
スタックビューのspacing
を0に設定すると、すべてが期待どおりに機能します。ただし、他の値に設定すると、レイアウトが壊れます。
ℹ️簡単にするために、スタックビューには
.fillProportionally
ディストリビューションを使用してUIStackView
は、次のようにシステム制約を作成します。
配置されたサブビューごとに、等しい幅制約(UISV-fill-proportionally
)を追加します。 multiplierを使用してスタックビュー自体に:
arrangedSubview[i].width = multiplier[i] * stackView.width
スタックビューにn配置されたサブビューがある場合、これらの制約のnを取得します。それらをproportionalConstraint[i]
と呼びましょう(ここで、i
はarrangedSubviews
配列内のそれぞれのビューの位置を示します)。
これらの制約は不要です(つまり、優先度は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
ディストリビューションの状態:
スタックビューが配置されたビューのサイズを変更して、スタックビューの軸に沿って使用可能なスペースを埋めるレイアウト。ビューは、スタックビューの軸に沿った固有のコンテンツサイズに基づいて比例してサイズ変更されます。
したがって、これによれば、multiplier
sのproportionalConstraint
は次のように計算する必要がありますspacing = 0
の場合:
totalIntrinsicWidth
= ∑私intrinsicWidth[i]
multiplier[i]
= intrinsicWidth[i]
/totalIntrinsicWidth
配置された10個のサブビューがすべて同じ固有の幅を持っている場合、これは期待どおりに機能します。
multiplier[i] = 0.1
すべてのproportionalConstraint
sに対して。ただし、間隔をゼロ以外の値に変更するとすぐに、間隔の幅を考慮する必要があるため、multiplier
の計算ははるかに複雑になります。私は数学を行い、multiplier[i]
の式は次のとおりです。
次のように構成されたスタックビューの場合:
上記の式は次のようになります。
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
でのみ正しく機能します。
他の間隔の場合、システムは誤った乗数で制約を作成します。
これはレイアウトを壊します
これを回避する唯一の方法は、ビュー間の間隔として必要な固定幅の制約を使用して、プレーンなUIView
sを「誤用」することです。 (通常、UILayoutGuide
sはこの目的で導入されましたが、スタックビューにレイアウトガイドを追加できないため、これらを使用することもできません。)
このバグが原因で、これを行うためのクリーンなソリューションがないのではないかと思います。
少なくともXcode9.2の時点では、初期化子と固有のコンテンツサイズが両方コメントアウトされていれば、提供されるプレイグラウンドは意図したとおりに機能します。その場合、間隔が0より大きい場合でも、比例充填は期待どおりに機能します。
これは間隔= 5の例です
配置されたサブビューには固有のコンテンツサイズがなく、StackViewは指定された軸を比例的に埋めるように幅を決定するため、これは理にかなっているようです。
初期化子のみが有効になっている場合(固有のコンテンツサイズのオーバーライドは有効になっていない場合)、これが表示されます。これはプレイグラウンドのコメントと一致しないため、質問が投稿されてからこの動作が変更されたに違いありません。
自動レイアウトを使用する場合、フレームを手動で設定することは無視する必要があるように思われるため、その動作を理解していません。
固有のコンテンツサイズのオーバーライドのみが有効になっている場合(初期化子は有効になっていない場合)、この投稿の元となった問題のある画像が表示されます(ここでは間隔= 5):
基本的に、指定された固有のコンテンツサイズのために、ビューの幅を1ポイントにする必要があるため、デザインが競合し、実現できません。ここの合計スペースは
24 views * 1 point/view + 23 spaces * 5 points/space = 139 < 300 = sv.bounds.width
mischaが指摘したように、優先度が最も低いため、最後に配置されたビューの制約が解除されます。
ピクセルごとの検査では、上記の最初の23ビューは1ピクセルよりも幅が広いですが、実際には最後のビューを除いて2または3ピクセルであるため、計算が完全に一致しません。理由はわかりません。おそらく切り上げます。 10進数の?
参考までに、これは、幅5の固有のコンテンツサイズを使用した場合のように見えますが、それでも制約を満たしていません。