SwiftUIにおけるForEach内からのBindingオブジェクトの渡し方

## はじめに

SwiftUIでリストコンテンツを表示するような画面を作る際に、`List`や`ForEach`等のビューを使用する場面があるかと思います。

下のコードでは、`RankingList`リストビューを持つ画面が、自身の`movies`配列の値を基に、リスト内の各行のビュー`RankingRow`を生成しています。

```{swift}

class RankingViewModel: ObservableObject {
    @Published var movies: [Movie]
    ...
}

struct RankingList: View {
    @ObservedObject var viewModel: RankingViewModel

    var body: some View {
        List {
            RankingHeader(&viewModel.genre)
            ForEach(viewModel.movies) { movie in
                RankingRow(movie: movie)
            }
        }
    }
}
```

この方法で`RankingRow`に渡しているのは`movies`配列の要素のコピーとなります。

```{swift}
ForEach(viewModel.movies) { movie in
    RankingRow(movie: movie) // Movieオブジェクトのコピーを渡している
}
```

`RankingRow`が`Movie`型の実体を受け取る設計であれば何ら問題ありませんが、もし`RankingRow`が`Movie`の`Binding`オブジェクトを受け取りたいとなった場合にはどうすればいいでしょうか。

## ForEachのイニシャライザ

次のように`viewModel.movies`の`Binding`オブジェクトをそのまま`ForEach`に渡したいところですが、残念ながらできません。

```{swift}
ForEach($viewModel.movies) {...} // NG: Binding<[Movie]>を渡そうとしている
ForEach(viewModel.$movies) {...} // NG: Published<[Movie]>.Publisherを渡そうとしている
```

定義を見てみると分かりますが、`ForEach`の3つのイニシャライザは`RandomAccessCollection`に準拠したオブジェクトか、もしくは`Range<Int>`型の値しか受け取らないためです。

```{swift}
struct ForEach<Data, ID, Content> where Data : RandomAccessCollection, ID : Hashable {

    // 1
    init(_ data: Data, @ViewBuilder content: @escaping (Data.Element) -> Content)

    // 2
    init(_ data: Data, id: KeyPath<Data.Element, ID>, content: @escaping (Data.Element) -> Content)

    // 3
    init(_ data: Range<Int>, @ViewBuilder content: @escaping (Int) -> Content)
}
// ※ 一部抜粋
```

何らかの配列を渡す方法を別途考えてみます。

## Array.indices

Swiftの`Array`には、自身の要素のインデックス範囲を返す`indices`プロパティがあります。

```{swift}
@inlinable public var indices: Range<Int> { get }
```

`Range<Int>`は上記3の`init(_, content:)`に渡すことのできる型ですが、`Range`自体が`RandomAccessCollection`に適合しているため、つまり上記1、2のイニシャライザにも渡すことができます。

今回はこの値を上記2の`init(_, id:, content:)`に渡すことで当初の問題を解決します。

```{swift}
ForEach(viewModel.movies.indices, id: \.self) {...}
```

## まとめ



```{swift}
class RankingViewModel: ObservableObject {
    @Published var movies: [Movie]
    ...
}

struct RankingList: View {
    @ObservedObject var viewModel: RankingViewModel

    var body: some View {
        List {
            RankingHeader(&viewModel.genre)
            ForEach(viewModel.movies.indices, id: \.self) { index in
                RankingRow(movie: self.$viewModel.movies[index]) // OK: Binding<Movie>を渡している！
            }
        }
    }
}
```

`ForEach`内で各要素の`index`を受け取ることができるようになった為、`viewModel.movies`配列の各要素を子ビューにバインディングして渡せるようになりました。

---

注意点として、`ForEach`生成時の引数に忘れずに`id:`を指定し、上記2の`init(_, id:, content:)`を呼び出す必要があります。

引数`id:`を指定しない場合、コンパイルは問題なく通って上記3の`init(_, content:)`を呼び出してしまうのですが、このイニシャライザは`Immutable`な`Range`オブジェクト向けとなっており、`Range`オブジェクトに変化があったとしてもSwiftUIはその変更をキャッチしてビューの更新を行ってくれません。

`id:`を指定せずに`Range<Int>`を渡した場合、初期化後のオブジェクトの更新時に以下のようなログが出力されます。

```
ForEach<Range<Int>, Int, RankingRow> count (3) != its initial count (0). `ForEach(_:content:)` should only be used for *constant* data. Instead conform data to `Identifiable` or use `ForEach(_: id:content:)` and provide an explicit `id`!
```

この警告が表示された際は、今一度`id:`を付け忘れていないか確認してみましょう。