Building Custom Views with SwiftUI まとめ
WWDC19のSwiftUI関連のビデオをまとめたいと思います。
今回はこちら
SwiftUIでのレイアウトの仕組みとグラフィックが解説されています。
今回はレイアウトの仕組みのみをまとめました。
プレゼンテーション資料はこちら
https://devstreaming-cdn.apple.com/videos/wwdc/2019/237x70rryl2b933v/237/237_building_custom_views_with_swiftui.pdf?dl=1
SwiftUIのレイアウトシステム
レイアウトは画面上の表示領域を決めること。
struct ContentView: View {
var body: some View {
Text("Hello World")
}
}
テンプレートでSwiftUIファイルを作成した場合のコードは上記です。
このとき表示されるビューは3つ。
- 最下層のTextビュー
- bodyビュー(Textビュー)と同じ領域のContentView
- Rootビュー(セーフゾーンは除いた領域)
補足として.edesIgnoringSafeArea(.all)
を使えばセーフエリア外にも領域を広げることが可能
struct ContentView: View {
var body: some View {
Text("Hello World")
.edgesIgnoringSafeArea(.all)
}
}
ContentViewはbodyビューの範囲で決まるので実質的にビューはTextビューとRootビューの2つを考えればよい。レイアウトのフローは以下の通り。
- 親ビューが子に対して表示可能領域を渡す(Rootビューがセーフエリア分の領域を子ビューのTextビューにわたす)
- 子ビューは自身のサイズを決める。(Textビューは自身のビュー領域をRootビューに返す)
- 親ビューが子ビューの表示位置を決める(RootビューはTextビューをデフォルトでビューの真ん中に配置する)
- SwiftUIがビューの角を最も近いピクセルに調整する。
SwiftUIでは子ビューのサイズは親ビューに左右されない。
親は表示可能領域と子の配置位置を決めて、子は自身のビュー領域を決める。
ビューは自身のビューのサイズを設定できることでビューごとのサイズ設定ができる。
なのでサイズ変更の方法やタイミングも自由にできる。
ビューの定義にサイズ設定が含まれる。
下記の例は画像をアスペクト比率1:1で指定する方法。
struct WedgeChart : View {
var body: some View {
Image("Wedges")
.aspectRatio(1)
}
}
ビューの変更
.background(Color.green)
をいれることでテキストの背景を緑にする。
パディングを入れるにはこう。
struct Toast : View {
var body: some View {
Text("Avocado Toast")
.padding()
.background(Color.green)
}
}
パディング量はプラットフォームやDynamic Typeのサイズ、その他環境に合わせて決定される。
ある方向のみデフォルトパディング量を入れることができる。(ハードコーディングが避けられる)
struct Toast : View {
var body: some View {
Text("Avocado Toast")
.padding([.leading, .trailing])
.background(Color.green)
}
}
.padding([.leading, .trailing])
で左右のパディングが作られた。
具体的な数値を指定もできる。上下左右すべての端から10ポイントのパディングを作りたいならこうする。
struct Toast : View {
var body: some View {
Text("Avocado Toast")
.padding(10)
.background(Color.green)
}
}
BackgroundとPaddingが追加されたときのビューレイアウトのプロセスをこのコードで解説する。
- RootビューがBackgroundへサイズを提案(セーフエリア分の領域)
- Backgroundは提案されたサイズをそのままPaddingに伝達
- PaddingビューはBackgroundから提案されたサイズよりも10ポイント左右上下小さいサイズを子ビューであるTextビューにわたす
- Textビューが自身のサイズを決定してPaddingビューに自身のビューサイズを伝達
- PaddingビューはTextから受け取ったビューよりも10ポイント大きな範囲を作りBackgroundに伝達し、Textビューを適切な位置に設定
- BackgroundはPaddingから渡されたビューサイズを変更しない。二次的な子ビューのColorにサイズを提案する
- Colorは自身でサイズを決定しないので提案されたビューサイズに従う。ColorサイズはPaddingサイズと同じになる
- 最後にBackgroundがRootビューに自身のサイズを伝達し、RootビューがBackgrondを真ん中に配置する
.でメソッドチェーンするということはつなげたビューが繋がれたビューの親ビューになるということを意味する
HStack and VStack
自動で文字の開始位置は調整される。アラビア語を設定すると右から左に変更される。
国際化対応は自動的に行われる。
HStackのレイアウトプロセスの解説
Hstackのレイアウトプロセルを解説する。
先程の Text("Avocado Toast").padding(10).background(Color.green)
の例は直列的なレイアウトプロセスだったが、HstackまたはVStackの子ビューたちはそれぞれ並列的な立場になっている。
- スタックビューは子ビューの間のスペースを計算し、親ビューから渡されたてビューサイズからスペース分のサイズをへらす。
S0 + S1が子ビューのスペース分のサイズ
- スタックビューの子ビューの数だけ(ここでは3つ)1で計算されたスペースを均等にわける。その一つを最も柔軟性のない子ビューへ渡す。この例ではサイズ固定のアボカド画像が選ばれる。
- アボカド画像はスタックから提案されたビューサイズから自分のサイズをスタックビューへ返す。
- スタックビューはアボカド画像分のビューサイズをビュースペース分から引く。
- スタックビューはビュースペースを残り2つのビュー分均等に分ける。そして残っているうちサイズの柔軟性のない 「Delicious」テキストビューにビューサイズを提案する。
- 「Delicious」テキストビューが自身のサイズビューをスタックビューに伝達する。
7.スタックビューは 「Delicious」テキストビューから伝達されたビューサイズ分をビュースペース分から引く。
- 残りのビュースペースを「Avocado Toast」テキストビューに提案する。「Avocado Toast」テキストビューは自身のサイズをスタックビューに伝達する。
- これで子ビューのサイズがすべて決定した。スタックビューはスペースを取って子ビューを並べる。
- スタックビューは子ビューの整列をする。指定がなければデフォルトでは縦方向の中央ぞろえにする。
HStack(alignment: .center)
のように指定もできる。
HStackの優先度を変更
スタックビューが十分なサイズを子ビューたちに対して提供できないとき、必要なテキストは優先的に表示し、その他のテキストは省略する方法を解説。デフォルトの優先度は0ですが、優先したいビューに対して.layoutPriority(1)
ととして優先度を1に変更する。
HStack {
Text("Delicious")
Image("20x20_avocado")
Text("Avocado Toast").layoutPriority(1)
}
.lineLimit(1)
スタックビューの子ビューの優先度が異なるとき、スタックビューは優先度が一番低いビューの最小サイズを確保して、優先度の高いビューに割り当てます。
HStackのbaselineをそろえる
HStack(alignment: .lastTextBaseline)
を指定することで、3つのビューのテキストベースラインをそろえることができる
HStack(alignment: .lastTextBaseline) {
Text("Delicious").font(.caption)
Image("20x20_avocado")
Text("Avocado Toast").layoutPriority(1)
}
.lineLimit(1)
さらにImage("20x20_avocado").alignmentGuide(.lastTextBaseline) { d in d[.bottom] * 0.927 }
とすることで独自の整列も指定できる。
HStack(alignment: .lastTextBaseline) {
Text("Delicious").font(.caption)
Image("20x20_avocado").alignmentGuide(.lastTextBaseline) { d in d[.bottom] * 0.927 }
Text("Avocado Toast").layoutPriority(1)
}
.lineLimit(1)
alignmentGuideメソッドはVerticalAlignmentで指定されたガイドの値が、computeValueクロージャーでもとのビュー(ここではImage)のViewDimensionsから修正した値になるような変更されたビューを返します。
func alignmentGuide(_ g: VerticalAlignment, computeValue: @escaping (ViewDimensions) -> CGFloat) -> some View
ViewDimensions
は自身のビュー領域のビューサイズと配置を表す型です。
.alignmentGuide(.lastTextBaseline) { d in d[.bottom] * 0.927 }
とすることで.lastTextBaseline
のbottomを0.927倍小さくしたものに調整することができます。
カスタム整列
星のラベルとタイトル「Abocado Toast」のラベルのセンターをあわせたい。
センターがあっていない。
HStack(alignment: .center) {
VStack {
Text("★★★★★")
Text("5 stars")
}.font(.caption)
VStack(alignment: .leading) {
HStack {
Text("Avocado Toast").font(.title)
Spacer()
Image("20x20_avocado")
}
Text("Ingredients: Avocado, Almond Butter, Bread, Red Pepper Flakes")
.font(.caption).lineLimit(1)
}
}
VerticalAlignment
をextensionする。
extension VerticalAlignment {
private enum MidStarAndTitle : AlignmentID {
static func defaultValue(in d: ViewDimensions) -> CGFloat {
return d[.bottom]
}
}
static let midStarAndTitle = VerticalAlignment(MidStarAndTitle.self)
}
MidStarAndTitle
のenumを作り、static func defaultValue(in d: ViewDimensions)
でデフォルトの値を返すようにする。ここでは.bottomの値を返す。
AlignmentID
はプロトコルで準拠すると整列ガイドの値とすることができる。その値は通常HorizontalAlignment
やVerticalAlignment
で宣言されている値のこと。
static let midStarAndTitle
でVerticalAlignment(MidStarAndTitle.self)
を指定している。
HStack(alignment: .midStarAndTitle)
でスタックの整列に.midStarAndTitle
を指定した上で、そろえたいビューに.alignmentGuide(.midStarAndTitle) { d in d[.bottom] / 2 }
を指定する。
.alignmentGuide(.midStarAndTitle) { d in d[.bottom] / 2 }
をすることでText("★★★★★")
とText("Avocado Toast")
の中心をそろえることができる。
HStack(alignment: .midStarAndTitle) {
VStack {
Text("★★★★★")
.alignmentGuide(.midStarAndTitle) { d in d[.bottom] / 2 }
Text("5 stars")
}.font(.caption)
VStack(alignment: .leading) {
HStack {
Text("Avocado Toast").font(.title)
.alignmentGuide(.midStarAndTitle) { d in d[.bottom] / 2 }
Spacer()
Image("alarmclock_30")
}
Text("Ingredients: Avocado, Almond Butter, Bread, Red Pepper Flakes")
.font(.caption).lineLimit(1)
}
}
d in d[.bottom]
はそのビューのトップからbottomまでの距離が入る。
なので{ d in d[.bottom] / 2 }
で半分にすると真ん中の配置位置を指定することができる。