Sponsor Link
環境&対象
- macOS Monterey 13 Beta9
- Xcode 14.1 beta 3
- iOS 16.0
SwiftUI の Layout
自分でレイアウトを作る時に使用する Protocol です。
Apple のドキュメントは、こちら。
最低限実装しないメソッドとまとめると、以下のような定義です。
@available(iOS 16.0, macOS 13.0, tvOS 16.0, watchOS 9.0, *)
public protocol Layout : Animatable {
func sizeThatFits(proposal: ProposedViewSize, subviews: Self.Subviews,
cache: inout Self.Cache) -> CGSize
func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize,
subviews: Self.Subviews, cache: inout Self.Cache)
}
以前の記事として、Layout の基本的な使い方をまとめています。
[SwfitUI] Layout を理解する
[SwiftUI] Layout を理解する/View のレイアウト情報取得
sizeThatFits
実装しなければいけないメソッドの1つ sizeThatFits について詳細を確認します。
func sizeThatFits(proposal: ProposedViewSize, subviews: Self.Subviews,
cache: inout Self.Cache) -> CGSize
与えられる引数は、proposal: ProposedViewSize, subviews: LayoutSubviews です。
(以下の説明では cache は省略してます)
引数 proposal: ProposedViewSize
1つ目の引数は、proposal: ProposedViewSize です。
親 Container からのレイアウトについてのリクエストを表しています。
以下のようなリクエストがありえます。
・特定のサイズにレイアウトしてほしい
・できるだけ小さいサイズにレイアウトしてほしい
・いっぱいの大きさを使ってレイアウトしてほしい
・自分で決めた大きさでレイアウトしてほしい
それぞれ、以下のような情報が、proposal にセットされてきます。
・width, height を持つ ProposedViewSize
・ProposedViewSize.zero
・ProposedViewSize.infinity
・ProposedViewSize.unspecified
あくまで、親 Container の要望なので、それに沿うかどうかは Layout 次第です。
対応具合はレイアウトによっていろいろだと思いますが、
例えば .zero 指定された時や、width/height 指定されたサイズに収まらないケースでは、一部のビュー表示を省略する等の対応も考えられそうです。
引数 subviews: LayoutSubviews
2つ目の引数は、LayoutSubviews です。
定義では、Self.Subviews になっていますが、LayoutSubviews の typealias です。
その名前の通り、子 View (の Proxy) です。(Proxy であり 子 View そのものではありません)
子 View の Proxy に対して、sizeThatFits メソッドを呼ぶことができますので、子View が必要とするサイズの確認をすることができます。
sizeThatFits が返す値(CGSize)
proposal で親 Container からの レイアウトに対する要望がわかり、subviews を参照することで、子 View の必要とするサイズが分かりますので、結果として、子 View を含めた 自分が必要とするサイズがわかることになります。
この情報が、sizeThatFits から 親 Container に返す情報となります。
placeSubviews
実装しなければいけないメソッドのもう1つ placeSubviews について詳細を確認します。
func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize,
subviews: Self.Subviews, cache: inout Self.Cache)
与えられる引数は、bounds: CGRect, proposal: ProposedViewSize, subviews: LayoutSubviews です。
(以下の説明では cache は省略してます)
sizeThatFits と比較すると、bounds: CGRect が増えています。
引数 bounds: CGRect
この bounds は、先に、sizeThatFits で自分の返した 大きさを持っていて、配置に使用することが想定されている CGRect です。
Apple のドキュメントにもありますが、この CGRect では、領域の原点を (0,0) に持たないことも考えられるので、常に、minX/midX/maxX, minY/midY/maxY 等を使用して計算しないといけません。
気をつけないといけないのが、与えられた bounds は、自分の返した大きさを持っていますが、親 Container が別の要素と干渉していないことを保証してくれるわけではないという点です。
干渉しない大きさについては、次の引数である proposal を参照することが必要となります。
引数 proposal: ProposedViewSize
2つ目の引数は、proposal: ProposedViewSize です。
proposal という名前ですが、他の View と衝突しない領域は、この proposal の領域です。
sizeThatFits に渡される proposal では、.zero / .infinity / .unspecified という特殊な値がありましたが、確認した範囲では、placeSubviews に渡される proposal は、実際に値を持っているようです。
あくまで、”確認した範囲” です。Apple のドキュメントには特に説明はありません。
引数 subviews: LayoutSubviews
3つ目の引数は、LayoutSubviews です。
この引数は、sizeThatFits に渡されてくる同名の引数と同じです。
注意点:placeSubviews
Apple のドキュメントには、proposal を変えて何度か呼び出される可能性があると説明されています。
当初、sizeThatFits がさまざまな組み合わせで何度も呼び出されると理解していたのですが、placeSubviews も様々な組み合わせで何度も呼び出されます。
親 Container としては、子 View から 複数パターンでの必要サイズを取得し、他の 子 View との調整を行った後に 一斉に placeSubviews を呼び出し配置すると “勝手に” 思っていましたが、実際の動作は違うようです。
同じ値を持つ引数を使った呼び出しも複数回確認できました。
sizeThatFits/placeSubviewsに与えられる proposal
placeSubviews の bounds の表す大きさは、sizeThatFits で返した大きさです。
このことは、Apple のドキュメントにも記載されています。
では、sizeThatFits/ placeSubviews に与えられる proposal は、どのような値が来るのでしょうか?
Apple のドキュメントには詳細の説明がないので、すこし試行錯誤しました。
以下の試行では、TestLayout が調査対象の Layout です。高さ方向のみに着目して確認しています。
親 Container の 子 View が 親 Container のサイズに収まる時
自分以外の View が要求する量が 親 Container の持つサイズに対して余裕があるケースでの sizeThatFits/placeSubviews に与えられるサイズを確認しました。
テストしたケース1
まずは、自 View 以外の高さが固定されているケースです。
struct ContentView: View {
var body: some View {
VStack(spacing: 0) {
Color.yellow.frame(height: 200).border(.red)
TestLayout {
Color.blue
}.border(.green)
Color.orange.frame(height: 200).border(.red)
}
.frame(width: 200, height: 500)
.padding()
}
}
全体の高さが 500 で、Color.yellow, Color.orange はそれぞれ高さ 200 なので、残りは 100 です。
sizeThatFits には、0.0, inf, 100.0 の高さを持つ proposal が渡されてきました。
placeSubviews に渡される proposal は、高さ100 のみでした。
他の 2つの View は、高さ固定なので、高さ 100 でのみ placeSubviews が呼び出されるのは、理解できます。

テストしたケース2
自 View 以外にも固定値でない制約が与えられている View が共存しているケースです。
struct ContentView: View {
var body: some View {
VStack(spacing: 0) {
Color.yellow.frame(idealHeight: 200).border(.red)
TestLayout {
Color.blue
}.border(.green)
Color.orange.frame(height: 200).border(.red)
}
.frame(width: 200, height: 500)
.padding()
}
}
全体の高さが 500 で、Color.orange の高さは、200 で固定です。
Color.yellow の理想値を 200 にしていますが、あくまで理想値です。
親 Container は、残りの高さ 300 を Color.yellow と 自 View に均等に割り当てようとしました。
sizeThatFits には、0.0, inf, 150.0 が渡されてきました。
placeSubviews に渡される proposal は、高さ 150 のみでした。

本題から少し外れますが、idealHeight を指定する意味がよくわからなくなりました。
TestLayout に高さ制約はなく、sizeThatFits に 0.0 が渡された時には、高さ 0.0 を返すようにしているので、Color.yellow に その理想的な高さ200を与えるように (自Layout には)高さ100 を与えてくると予想していたのですが、与えてきませんでした。
少なくともこのケースでは、idealHeight が指定されていても、特にその値を割り当てるよう努力しているように思えません。全てのケースを考慮できているわけではないので、idealHeight が活躍するケースが他にあるのかもしれません。ご存知の方 教えてくださると嬉しいです。
親 Container の 子 View が収まらない時
子 View の要求するサイズが収まらない時には、どうなるのかも確認しました。
テストしたケース3
自分以外の View が要求する量が すでに 親 Container をいっぱいにするケースです。
struct ContentView: View {
var body: some View {
VStack(spacing: 0) {
Color.yellow.frame(height: 200).border(.red)
TestLayout {
Color.blue
}.border(.green)
Color.orange.frame(height: 300).border(.red)
}
.frame(width: 200, height: 500)
.padding()
}
}
sizeThatFits への proposal は、0.0, .infinity が与えられてきます。
placeSubviews での proposal は、高さ 0 です。
以下のような配置になり、Color.blue は、高さ 0 で配置されます。つまり、見えません。

テストしたケース4
自分以外の View が要求する量が すでに 親 Container をいっぱいにするケースです。ただし、別の View は、idealHeight での指定のケース。
struct ContentView: View {
var body: some View {
VStack(spacing: 0) {
Color.yellow.frame(idealHeight: 200).border(.red)
TestLayout {
Color.blue
}.border(.green)
Color.orange.frame(height: 300).border(.red)
}
.frame(width: 200, height: 500)
.padding()
}
}
sizeThatFits への proposal には、0.0, inf, 100 が与えられました。
placeSubviews での proposal は、高さ 100 でした。
Color.orange の高さは、300で固定なので、のこりの高さ (500 – 300) = 200 を Color.yellow と Color.blue で分けるような proposal になっていると考えられます。
動作的には ケース2と同じです。
ここでも、idealHeight については、あまり考慮されていないように見えます。

考察
たった 4ケースですが、動作を確認してみました。
厳密にはもっと調べなければいけませんが、以下のことが言える気がします。
・sizeThatFits には、0.0, .inf と合わせて、他の View との兼ね合いを考慮した 自 View に与えることができる最大サイズ が渡される。
(sizeThatFits から返す値で変わるかもしれませんが)
・placeSubviews には、他の View との兼ね合いを考慮した最大サイズが、渡される。
いずれも、”気”がするだけですが・・・
まとめ
Layout で使われる sizeThatFits と placeSubviews に渡される引数の意味を調べてみました。
- placeSubviews に渡される bounds は、sizeThatFits が返した大きさを 親 Container が考える配置予定の箇所に配置した時の bounds であり、他 View との干渉がないことを 親 Container が保証してくれることはなさそう
- placeSubviews に渡される proposal は、.zero や .infinity ではなく、実際の値がセットされた ProposedViewSize が渡される気がする
- 他 View との干渉なく配置したい時には、bounds で渡される領域のうち、proposal 分のみで配置することが必要となる気がする
説明は以上です。
不明な点やおかしな点ありましたら、こちらまで。
SwiftUI おすすめ本
SwiftUI を理解するには、以下の本がおすすめです。
SwiftUI ViewMatery
SwiftUI で開発していくときに、ViewやLayoutのための適切なmodifierを探すのが大変です。
英語での説明になってしまいますが、以下の”SwiftUI Views Mastery Bundle”という本がビジュアル的に確認して探せるので、便利です。
英語ではありますが、1ページに コードと画面が並んでいるので、非常にわかりやすいです。
View に適用できる modifier もわかりやすく説明されているので、ビューの理解だけではなく、どのような装飾ができるかも簡単にわかります。
超便利です
販売元のページは、こちらです。
SwiftUI 徹底入門
# SwiftUI は、毎年大きく改善されていますので、少し古くなってしまいましたが、いまでも 定番本です。
Swift学習におすすめの本
詳解Swift
Swift の学習には、詳解 Swift という書籍が、おすすめです。
著者は、Swift の初期から書籍を出していますし、Swift の前に主力言語だった Objective-C という言語についても同様の書籍を出しています。
最新版を購入するのがおすすめです。
現時点では、上記の Swift 5 に対応した第5版が最新版です。
Sponsor Link