🤖

[SwiftUI] ForEachから学ぶイニシャライザ

2024/07/30に公開

概要

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.ElementIdentifiableの制約をつけることで、IDという関連型にアクセスできるようになります。
そしてそのIDHashableに適合しているので、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 にアップロードしています。

GitHubで編集を提案

Discussion