Open5

SwiftUIのForEach。あいつはViewの皮をかぶった良いヤツだった。

1amageek1amageek

SwiftUIでよく使われるForEach。
HStack, VStack, ZStack, Picker, List,に含めると複数の要素を表現できる。この使い方がとっても疑問だった。

1amageek1amageek

SwiftUIを使うとき当然のようにこう書いているが

VStack {
  ForEach(0..<10) { index in
    Text("\(index)")
  }
}

Listだとこう書けたりする

List(0..<10) { index in
  Text("\(index)")
}

コンポーネントを作るならばこの方が自然だったりする。
Listコンポーネントは0..<10のデータに依存し、そのデータをイテレーターが回す。

だが実際、SwiftUIではこの二つの構文はほとんど差がない。
それを理解するためにはSwiftの FunctionBuilder の仕組みと ViewBuilder を理解する必要がある、ちょっと大変な説明になるのでここでは次のように説明する

func myFunction(a: A, b: B, c: C)
func myFunction(@AnyBuilder content: () -> [A, B, C])

めっちゃ簡単に言うと、引数を 動的 に作り替えることができる機能。
※動的と書いてるけど、実際は静的。開発者側から見ると動的に見えるだけで、実際は静的に方が決定してる。

ここがSwiftの本当にすごいところ。マジですごい。天才的だし、ホント感動するレベル。

1amageek1amageek

ここでちょっとSwiftUIのForEachの宣言をのぞいてみよう。

さらにHStack

さらにList

お分かり頂けただろうか?

ここでは3つのことに注目してほしい

  1. ForEachにはプロパティがある
  2. ForEachは最初の宣言でView Protocolに準拠していない
  3. イニシャライザーがない

この3つのことについて説明していく

ForEachにはプロパティがある

SwiftUIのコンポーネントの中でプロパティを持つのは非常に珍しい。ちゃんと見てないけどもしかしたらForEachだけだったりするかも。
これは何を意味するのか、結論から言うと、どこか内部で何かにプロパティを参照されることを前提に設計されていると言うことだ。

ForEachは最初の宣言でView Protocolに準拠していない

SwiftUIのコンポーネントならばViewに準拠させるのが当然っしょ!と思いきやForEachは違う。
純粋なStructとして定義されている。

実際はextensionとしてViewに準拠する形になっている。その宣言部を見てみる。

このようにViewに準拠はしているがBodyにはNeverがセットされている、つまりForEachはBodyを持たない。

イニシャライザーがない

最初の宣言にはイニシャライザがないが、実際はForEachには多くのイニシャライザが宣言されている。
これはForEachにはさまざまな型が宣伝できるように設計されておりGenericsを使って柔軟にイニシャライザを使えるような設計になっている。

SwiftUIのViewBuilderの中で使えるようViewに準拠しているが、ForEach自体はただ情報を親のViewに伝達することが目的であることがわかる。

1amageek1amageek

例えば次のように宣言したときどのようにすれば、ForEachを扱うことができるのか考えてみる。

MyView {
  ForEach(0..<10) { index in
    Text("\(index)")
  }
}

イニシャライザでwhereを使ってGenericsの型をForEachに固定することで安全にForEachを取得することができる。

struct MyView<Content>: View where Content: View { }

extension MyView {
    init<Data, ID, InContent>(@ViewBuilder content: () -> Content) where Content == ForEach<Data, ID, InContent>, Data: RandomAccessCollection, ID: Hashable, InContent: View {
        let forEach = content()
        let data = forEach.data
        let content = forEach.content
    }
}

また、ViewBuilderに複数引数がある場合はTupleViewを使う

extension MyView {
    init<Data, ID, InContent, OtherContent>(@ViewBuilder content: () -> Content) where Content == TupleView<(ForEach<Data, ID, InContent>, OtherContent)>, Data: RandomAccessCollection, ID: Hashable, InContent: View, OtherContent: View {
        let tupleView = content()
        let forEach = tupleView.value.0
        let data = forEach.data
        let content = forEach.content
    }
}