iOS, Swift, SwiftUI

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のレイアウトシステム

レイアウトは画面上の表示領域を決めること。

----------2019-08-20-9.27.09

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つを考えればよい。レイアウトのフローは以下の通り。

  1. 親ビューが子に対して表示可能領域を渡す(Rootビューがセーフエリア分の領域を子ビューのTextビューにわたす)
  2. 子ビューは自身のサイズを決める。(Textビューは自身のビュー領域をRootビューに返す)
  3. 親ビューが子ビューの表示位置を決める(RootビューはTextビューをデフォルトでビューの真ん中に配置する)
  4. SwiftUIがビューの角を最も近いピクセルに調整する。

----------2019-08-20-9.50.39

----------2019-08-20-9.50.46
----------2019-08-20-9.52.35

SwiftUIでは子ビューのサイズは親ビューに左右されない。

親は表示可能領域と子の配置位置を決めて、子は自身のビュー領域を決める。

ビューは自身のビューのサイズを設定できることでビューごとのサイズ設定ができる。
なのでサイズ変更の方法やタイミングも自由にできる。

ビューの定義にサイズ設定が含まれる。
下記の例は画像をアスペクト比率1:1で指定する方法。

struct WedgeChart : View {
 var body: some View {
 Image("Wedges")
 .aspectRatio(1)
 }
}

ビューの変更

----------2019-08-20-10.07.30

.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が追加されたときのビューレイアウトのプロセスをこのコードで解説する。

----------2019-08-20-10.16.59

  1. RootビューがBackgroundへサイズを提案(セーフエリア分の領域)
  2. Backgroundは提案されたサイズをそのままPaddingに伝達
  3. PaddingビューはBackgroundから提案されたサイズよりも10ポイント左右上下小さいサイズを子ビューであるTextビューにわたす
  4. Textビューが自身のサイズを決定してPaddingビューに自身のビューサイズを伝達
  5. PaddingビューはTextから受け取ったビューよりも10ポイント大きな範囲を作りBackgroundに伝達し、Textビューを適切な位置に設定
  6. BackgroundはPaddingから渡されたビューサイズを変更しない。二次的な子ビューのColorにサイズを提案する
  7. Colorは自身でサイズを決定しないので提案されたビューサイズに従う。ColorサイズはPaddingサイズと同じになる
  8. 最後にBackgroundがRootビューに自身のサイズを伝達し、RootビューがBackgrondを真ん中に配置する

.でメソッドチェーンするということはつなげたビューが繋がれたビューの親ビューになるということを意味する


HStack and VStack

自動で文字の開始位置は調整される。アラビア語を設定すると右から左に変更される。
国際化対応は自動的に行われる。

----------2019-08-20-10.34.42

HStackのレイアウトプロセスの解説

----------2019-08-20-20.13.03

Hstackのレイアウトプロセルを解説する。
先程の Text("Avocado Toast").padding(10).background(Color.green) の例は直列的なレイアウトプロセスだったが、HstackまたはVStackの子ビューたちはそれぞれ並列的な立場になっている。

  1. スタックビューは子ビューの間のスペースを計算し、親ビューから渡されたてビューサイズからスペース分のサイズをへらす。

----------2019-08-20-20.20.00

----------2019-08-20-20.20.10

S0 + S1が子ビューのスペース分のサイズ

  1. スタックビューの子ビューの数だけ(ここでは3つ)1で計算されたスペースを均等にわける。その一つを最も柔軟性のない子ビューへ渡す。この例ではサイズ固定のアボカド画像が選ばれる。

----------2019-08-20-20.24.09

  1. アボカド画像はスタックから提案されたビューサイズから自分のサイズをスタックビューへ返す。

----------2019-08-20-20.24.21

  1. スタックビューはアボカド画像分のビューサイズをビュースペース分から引く。

----------2019-08-20-20.27.38

  1. スタックビューはビュースペースを残り2つのビュー分均等に分ける。そして残っているうちサイズの柔軟性のない 「Delicious」テキストビューにビューサイズを提案する。

----------2019-08-20-20.30.36

  1. 「Delicious」テキストビューが自身のサイズビューをスタックビューに伝達する。

----------2019-08-20-20.31.18

7.スタックビューは 「Delicious」テキストビューから伝達されたビューサイズ分をビュースペース分から引く。

----------2019-08-20-20.34.04

  1. 残りのビュースペースを「Avocado Toast」テキストビューに提案する。「Avocado Toast」テキストビューは自身のサイズをスタックビューに伝達する。

----------2019-08-20-20.38.14

  1. これで子ビューのサイズがすべて決定した。スタックビューはスペースを取って子ビューを並べる。

----------2019-08-20-20.40.17

  1. スタックビューは子ビューの整列をする。指定がなければデフォルトでは縦方向の中央ぞろえにする。

HStack(alignment: .center) のように指定もできる。

----------2019-08-20-20.41.50

HStackの優先度を変更

スタックビューが十分なサイズを子ビューたちに対して提供できないとき、必要なテキストは優先的に表示し、その他のテキストは省略する方法を解説。デフォルトの優先度は0ですが、優先したいビューに対して.layoutPriority(1)ととして優先度を1に変更する。

HStack {
 Text("Delicious")
 Image("20x20_avocado")
 Text("Avocado Toast").layoutPriority(1)
}
.lineLimit(1)

スタックビューの子ビューの優先度が異なるとき、スタックビューは優先度が一番低いビューの最小サイズを確保して、優先度の高いビューに割り当てます。

----------2019-08-20-20.51.36

HStackのbaselineをそろえる

HStack(alignment: .lastTextBaseline)を指定することで、3つのビューのテキストベースラインをそろえることができる

HStack(alignment: .lastTextBaseline) {
 Text("Delicious").font(.caption)
 Image("20x20_avocado")
 Text("Avocado Toast").layoutPriority(1)
}
.lineLimit(1)

----------2019-08-20-22.47.24

さらに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)

----------2019-08-20-22.55.08

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」のラベルのセンターをあわせたい。

----------2019-08-20-23.07.07

センターがあっていない。

----------2019-08-20-23.08.47

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はプロトコルで準拠すると整列ガイドの値とすることができる。その値は通常HorizontalAlignmentVerticalAlignmentで宣言されている値のこと。
static let midStarAndTitleVerticalAlignment(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までの距離が入る。
----------2019-08-20-23.36.57

なので{ d in d[.bottom] / 2 }で半分にすると真ん中の配置位置を指定することができる。

----------2019-08-20-23.43.16

Author image

About Sato Takeshi

  • Tokyo, Japan