SwiftUI の Layout System について調べてみる🤔( DeNA 24 新卒 Advent Calendar 2023 Day 1)

Summer
11 min readNov 30, 2023

--

この記事は DeNA 24 新卒 Advent Calendar 2023 の 1 日目の記事です🎉🎉 これから 25 日間毎日記事が投稿されるので.ぜひチェックしてみてください!

DeNA 24 新卒のカレンダー | Advent Calendar 2023 — Qiita

はじめに

近年 iOS 開発の現場でも広く利用され始めている SwiftUI のレイアウトシステムについて興味を持ったので,そのあたりを調べてみることにしました!それではよろしくお願いします!

実行環境

MacBook Pro 13-inch, M1, 2020
macOS: Venture 13.4.1
Simulator: iPhone 14 Pro

SwiftUI におけるレイアウトプロセスの基本

WWDC の動画によると,レイアウトのプロセスは次の通りです.

  1. 親の View が子の View にサイズを提案
  2. 子の View が自身のサイズを決定
  3. 親の View が子の View を自身の座標空間内に配置

非常にシンプルですね!
そしてこのプロセスの中で注意が必要なのは,

  • View によって自身のサイズの決め方は異なり,親のからのサイズの提案に従わない場合もある.
  • ステップ 3 において,親は子がステップ 2 で決定したサイズを必ず尊重する.

ということです.
自分が望むレイアウトが得られない場合には,この 2 点を意識すると原因と改善策が見つかるかも知れません 🤔

具体的な View の挙動

単純な View

いくつかの単純な View(Text, Image, Color) に .frame を利用してサイズを与えどのように自身のサイズを決定しているのかを確認してみます.

Text

縦横ともに十分なスペースがある場合
自身を表示する必要最低限のサイズを確保していることが分かります.

Text("Advent Calendar 2023 Day 1")
.border(.blue)
.frame(width: 300, height: 300)
.border(.red)

縦にはスペースがあるが横にはない場合
与えられた幅を超える場合は折り返します.

Text("これはアドベントカレンダー1日目の記事です.ぜひ購読して毎日チェックしてください!")
.border(.blue)
.frame(width: 300, height: 300)
.border(.red)

縦横ともにスペースがない場合(height を 30 に制限)
入りきらない部分については … の表示により省略されます.

Color

提案されたスペースを全て覆うように表示されます.

Color.blue
.border(.red)
.frame(width: 300, height: 300)

Image

十分なスペースがある
フルサイズの画像を表示します.

Image("300x300")
.frame(width: 350, height: 350)
.border(.red)

十分なスペースがない
これは親のサイズの提案に従わない例になります.
Image はフルサイズの画像を表示する View であるため,frame(親)の提案に関係なく自身のサイズを 300x300 に決定します.

Image("300x300")
.frame(width: 250, height: 200)
.border(.red)

十分なスペースがない(.resizable を追加)
ちなみに .resizable を追加することにより Image を柔軟な View に変えることができます.

Image("300x300")
.resizable()
.frame(width: 250, height: 200)
.border(.red)

さらに .aspectRatio を追加することで,アスペクト比を保ったままサイズを柔軟に変えることができます.

十分なスペースがない(.resizable().aspectRatio(contentMode: .fit)を追加)
アスペクト比を保ったまま与えられたスペース内に収まるようリサイズされます.

Image("300x300")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 250, height: 200)
.border(.red)

十分なスペースがない(.resizable().aspectRatio(contentMode: .fill)を追加)
アスペクト比を保ったまま与えられたスペースを全て覆うようリサイズされます.

Image("300x300")
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width: 250, height: 200)
.border(.red)

ここまでで,

  • View によって自身のサイズの決め方は異なる
  • 子の View が親の View の提案に従わない場合がある

ということが具体的に確認できたかと思います.

HStack, VStack

続いて,複数の子の View を持つ View である Stack の挙動についてです.
HStack, VStack 内のレイアウトが構築される方針は次のようになります.

  • 提案されたサイズから,内部の View の間隔として必要なサイズを引く
  • 基本は子の View に提供可能なスペースを均等に分割して提案する
  • 柔軟でない View を子に持つ場合は,それらの View からサイズを決めていく
  • Priority に差がある場合は,優先度が下位の View の最小幅を確保した上で,優先度が上位の View に優先的にサイズを割り当てる
  • 全ての子のサイズが決まり次第,自身のサイズをそれらを囲うのに必要十分なサイズに決定する.

HStack を利用してこれらの挙動を確認してみます.

HStack

HStack は子の View を水平方向に並べることができます.
次の例では,子の View の間にスペースが確保されていることと,HStack 自身のサイズは全ての子の View を囲うのに必要十分なサイズであることが確認できます.

HStack {
Text("🍎🍎🍎")
.border(.green)
Text("りんごが 3 つ")
.border(.orange)
}
.border(.blue)
.frame(width: 300, height: 40)
.border(.red)

文字列を長くしてみる
それぞれの文字列を長くしてみると,いずれの文字列も … と省略されるようになり,同程度のスペースに分割されることが確認できます.

HStack {
Text("🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎")
.border(.green)
Text("りんごがたくさん!!!")
.border(.orange)
}
.border(.blue)
.frame(width: 300, height: 40)
.border(.red)

柔軟でない View として,Image を追加してみる
次の例では,Image はフルサイズで確保され,その上で残りのスペースを二つの Text が分割していることを確認できます.また,子の View の間隔が保たれ続けていることと,最も高さの大きい Image に合わせて HStack の高さも高くなっていることも確認できます.

HStack {
Image("150x60")
Text("🍎🍎🍎")
.border(.green)
Text("りんごが 3 つ")
.border(.orange)
}
.border(.blue)
.frame(width: 300, height: 40)
.border(.red)

.layoutPriority を使ってみる
.layoutPriority を追加することで,View の優先度を設定することができます.引数には Double の値をとり,デフォルトの値は 0 です.より大きな値を持つ View から優先的にサイズを決定します.

HStack {
Text("🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎")
.border(.green)
Text("りんごがたくさん!!!")
.layoutPriority(1)
.border(.orange)
}
.border(.blue)
.frame(width: 300, height: 40)
.border(.red)

-1 を指定することで,優先度を下げることもできます.

HStack {
Text("🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎")
.border(.green)
Text("りんごがたくさん!!!")
.layoutPriority(-1)
.border(.orange)
}
.border(.blue)
.frame(width: 300, height: 40)
.border(.red)

柔軟ではない View と .layoutPriority によって優先度を高めた View の比較
この場合は,柔軟ではない Image のサイズ確保が優先されるようです.

HStack {
Image("150x60")
Text("🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎")
.border(.green)
Text("りんごがたくさん!!!")
.layoutPriority(1)
.border(.orange)
}
.border(.blue)
.frame(width: 300, height: 40)
.border(.red)

まとめ

SwiftUI のレイアウトが,

  1. 親の View が子の View にサイズを提案
  2. 子の View が自身のサイズを決定
  3. 親の View が子の View を自身の座標空間内に配置

という 3 つのシンプルなプロセスによって構成されることを見てきました.このプロセスに従うことにより,SwiftUI は矛盾なくレイアウトを構成することができます.今後,思うように View が表示できない場面に出会したときは,これらのプロセスと

  • View によって自身のサイズの決め方は異なる
  • 子の View が親の View の提案に従わない場合がある

ということに注意して,考え直してみようと思います!

最後まで目を通していただきありがとうございました!
明日以降の記事もぜひ楽しみにしておいてください😆😆😆

--

--