SwiftUIのForEach。あいつはViewの皮をかぶった良いヤツだった。
SwiftUIでよく使われるForEach。
HStack
, VStack
, ZStack
, Picker
, List
,に含めると複数の要素を表現できる。この使い方がとっても疑問だった。
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の本当にすごいところ。マジですごい。天才的だし、ホント感動するレベル。
ここでちょっとSwiftUIのForEachの宣言をのぞいてみよう。
さらにHStack
さらにList
お分かり頂けただろうか?
ここでは3つのことに注目してほしい
- ForEachにはプロパティがある
- ForEachは最初の宣言で
View Protocol
に準拠していない - イニシャライザーがない
この3つのことについて説明していく
ForEachにはプロパティがある
SwiftUIのコンポーネントの中でプロパティを持つのは非常に珍しい。ちゃんと見てないけどもしかしたらForEachだけだったりするかも。
これは何を意味するのか、結論から言うと、どこか内部で何かにプロパティを参照されることを前提に設計されていると言うことだ。
View Protocol
に準拠していない
ForEachは最初の宣言でSwiftUIのコンポーネントならばView
に準拠させるのが当然っしょ!と思いきやForEachは違う。
純粋なStructとして定義されている。
実際はextensionとしてView
に準拠する形になっている。その宣言部を見てみる。
このようにView
に準拠はしているがBodyにはNeverがセットされている、つまりForEachはBodyを持たない。
イニシャライザーがない
最初の宣言にはイニシャライザがないが、実際はForEachには多くのイニシャライザが宣言されている。
これはForEachにはさまざまな型が宣伝できるように設計されておりGenericsを使って柔軟にイニシャライザを使えるような設計になっている。
SwiftUIのViewBuilderの中で使えるようViewに準拠しているが、ForEach自体はただ情報を親のViewに伝達することが目的であることがわかる。
独自のView
でForEach
を使うときはどうすれば良いのか?
ForEach
を使ってSwiftUIには存在しない複数のComponentsを持つPickerを作ってみたので参考にして欲しい。
例えば次のように宣言したときどのようにすれば、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
}
}