[SwiftUI] ForEachから学ぶイニシャライザ
概要
SwiftUI には、識別可能なデータ群をループし View を計算するForEachという API が存在し、ForEach
には以下のように大きく2つの I/F が存在します。
そしてこの2つの違いは以下になります。
- ジェネリックパラメータの Data がIdentifiableに準拠しているか
- 準拠していない場合は、
KeyPath<Data.Element, ID>
を引数に取るかどうか
本記事では、どのようにして KeyPath
の有無に左右されない実装になっているのか考察していきます。
事前情報
考察するにあたって以下のような横スクロール可能な View コンポーネントを例に見ていきます。
パラメータを考える
まず View 側でどのようなパラメータを扱うかを決定します。
struct HorizontalScrollView<Data, ID, Content>: View
where
Data: RandomAccessCollection, // ForEachで取り扱えるようにするために、RandomAccessCollectionに準拠しておく
ID: Hashable, // ForEachのKeyPathに渡せるようにHashableに準拠しておく
Content: View
{
private let data: Data
private let id: KeyPath<Data.Element, ID>
private let content: (Data.Element) -> Content
var body: some View {
// あとで実装します
}
}
基本的には、ForEachを使用するときに必要なデータだけを渡しておきます。
Identifiable に準拠しないイニシャライザ
次はinit(_:id:content:)相当の、データがIdentifiable
に準拠せず、KeyPath<Data.Element, ID>
を引数に取るイニシャライザを実装します。
init(
data: Data,
id: KeyPath<Data.Element, ID>,
@ViewBuilder content: @escaping (Data.Element) -> Content
) {
self.data = data
self.id = id
self.content = content
}
Identifiable に準拠するイニシャライザ
次はinit(_:content:)相当の、データが Identifiable に準拠するイニシャライザを実装します。
struct に convinience initializer を追加したいので、extension を生やします。
extension HorizontalScrollView {
init(
data: Data,
@ViewBuilder content: @escaping (Data.Element) -> Content
) where Data.Element: Identifiable, Data.Element.ID == ID {
self.data = data
self.id = \.id
self.content = content
}
}
ここでの注目ポイントは、Data.Element: Identifiable
の制約と、Data.Element.ID == ID
の同型制約をつけている点です。
まずData.Element
にIdentifiable
の制約をつけることで、ID
という関連型にアクセスできるようになります。
そしてそのID
はHashable
に適合しているので、HorizontalScrollView のID
パラメータの制約にも適合します。
しかし以下のエラーが発生し、KeyPath<Data.Element, ID>
のパラメータを初期化することができません。
Cannot assign value of type 'KeyPath<Data.Element, Data.Element.ID>' to type 'KeyPath<Data.Element, ID>'
そこでData.Element.ID == ID
の同型制約をつけることで、KeyPath<Data.Element, ID>
を満たすことができるようになります。
横スクロールを実装
あとは、KeyPath<Data.Element, ID>
の ForEach のイニシャライザを使って横スクロールを実装するだけです。
var body: some View {
ScrollView(.horizontal, showsIndicators: false) {
LazyHStack(spacing: 16) {
ForEach(data, id: id) { content($0) }
}
.padding(.horizontal, 16)
}
}
まとめ
Identifiable
に準拠しない・するイニシャライザをそれぞれ用意したい場合、KeyPath<Data.Element, ID>
を用いた実装をベースとし、Identifiable
に適合したデータからKeyPath<Data.Element, ID>
を生成してやれば、両方を満たすことができるようになりました。
今回実装した内容は、GitHub にアップロードしています。
Discussion