Issue #796
One seemingly obvious way to use ForEach on array with indices is using enumerated
struct Book: Hashable, Identifiable {
let id: UUID
let name: String
}
struct BooksView: View {
let books: [Book]
var body: some View {
List {
ForEach(Array(books.enumerated()), id: \.element) { index, book in
Text(book.name)
.background(index % 2 == 0 ? Color.green : Color.orange)
}
}
}
}
Reading the documentation for enumerated closely
When you enumerate a collection, the integer part of each pair is a counter for the enumeration, but is not necessarily the index of the paired value. These counters can be used as indices only in instances of zero-based, integer-indexed collections, such as Array and ContiguousArray. For other collections the counters may be out of range or of the wrong type to use as an index. To iterate over the elements of a collection with its indices, use the zip(::) function.
We see that it works only for 0 based indexed collections like Array
, a safer solution proposed in the docs is to use zip
. Since our model conforms to Identifiable
, we use id: \.1
to tell ForEach
to use our model as identifiers. Here we can safely use index
in the closure as it points to the actual index in the books
collection
struct BooksView: View {
let books: [Book]
var body: some View {
List {
ForEach(Array(zip(books.indices, books)), id: \.1) { index, book in
Text(book.name)
.background(index % 2 == 0 ? Color.green : Color.orange)
}
}
}
}
I often encapsulate this logic into a reusable View
struct ForEachWithIndex<
Data: RandomAccessCollection,
Content: View
>: View where Data.Element: Identifiable, Data.Element: Hashable {
let data: Data
@ViewBuilder let content: (Data.Index, Data.Element) -> Content
var body: some View {
ForEach(Array(zip(data.indices, data)), id: \.1) { index, element in
content(index, element)
}
}
}
so we can use easily
ForEachWithIndex(data: viewModel.books) { index, book in
VStack {
BookRow(book: book)
if index < viewModel.books.count - 1 {
Divider()
}
}
}
We can also declare it as a free function to return ForEach
directly
func ForEachWithIndex<
Data: RandomAccessCollection,
Content: View>(
_ data: Data,
@ViewBuilder content: @escaping (Data.Index, Data.Element) -> Content
) -> some View where Data.Element: Identifiable, Data.Element: Hashable {
ForEach(Array(zip(data.indices, data)), id: \.1) { index, element in
content(index, element)
}
}