Here's a simple SwiftUI List that works as expected:
struct App: View { let items = Array(100...200) var body: some View { List { ForEach(items, id: \.self) { index, item in Text("Item \(item)") } }.frame(width: 200, height: 200) } }
but when I try to enumerate items by replacing items
with items.enumerated()
I get these errors:
Referencing initializer 'init(_:id:content:)' on 'ForEach' requires that '(offset: Int, element: Int)' conform to 'Hashable'
Referencing initializer 'init(_:id:content:)' on 'ForEach' requires that 'EnumeratedSequence<[Int]>' conform to 'RandomAccessCollection'
How do I make this work?
An enumeration defines a common type for a group of related values and enables you to work with those values in a type-safe way within your code.
TL;DR
Warning: If you get in the habit of using enumerated()
with ForEach
, you may one day end up with EXC_BAD_INSTRUCTION
or Fatal error: Index out of bounds
exceptions. This is because not all collections have 0-based indexes.
A better default is to use zip
instead:
ForEach(Array(zip(items.indices, items)), id: \.0) { index, item in // index and item are both safe to use here }
(You can also use id: \.1
if your items conform to Identifiable
.)
The folks over at Point-Free mentioned that it's not safe to rely on enumerated()
with ForEach
in production since not all collections are zero-index based:
This is technically not the most correct way to do this. It would be more correct, and more verbose, to zip the
todos
array with its indices collection. In this case we are safe because we are dealing with a simple 0-based index array, but if we were doing this in production we should probablyzip
-based approach.
Apple's documentation for the enumerated function mentions this as well:
/// Returns a sequence of pairs (*n*, *x*), where *n* represents a /// consecutive integer starting at zero and *x* represents an element of /// the sequence. /// /// This example enumerates the characters of the string "Swift" and prints /// each character along with its place in the string. /// /// for (n, c) in "Swift".enumerated() { /// print("\(n): '\(c)'") /// } /// // Prints "0: 'S'" /// // Prints "1: 'w'" /// // Prints "2: 'i'" /// // Prints "3: 'f'" /// // Prints "4: 't'" /// /// 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. /// /// This example iterates over the indices and elements of a set, building a /// list consisting of indices of names with five or fewer letters. /// /// let names: Set = ["Sofia", "Camilla", "Martina", "Mateo", "Nicolás"] /// var shorterIndices: [Set<String>.Index] = [] /// for (i, name) in zip(names.indices, names) { /// if name.count <= 5 { /// shorterIndices.append(i) /// } /// } /// /// Now that the `shorterIndices` array holds the indices of the shorter /// names in the `names` set, you can use those indices to access elements in /// the set. /// /// for i in shorterIndices { /// print(names[i]) /// } /// // Prints "Sofia" /// // Prints "Mateo" /// /// - Returns: A sequence of pairs enumerating the sequence. /// /// - Complexity: O(1)
In your specific case enumerated()
is fine to use since you are using a 0-based index array, however due to the details above, relying on enumerated()
all the time can lead to non-obvious errors.
Take this snippet, for example:
ForEach(Array(items.enumerated()), id: \.offset) { offset, item in Button(item, action: { store.didTapItem(at: offset) }) } // ... class Store { var items: ArraySlice<String> func didTapItem(at index: Int) { print(items[index]) } }
First notice that we dodged a bullet with Button(item...
since enumerated()
has guaranteed that item
can be accessed directly without causing an exception. However, if instead of item
we used items[offset]
, an exception could easily be raised.
Finally, the line print(items[index])
can easily lead to an exception since the index (really the offset) can be out of bounds.
Therefore, a safer approach is to always use the zip
method mentioned at the top of this post.
Another reason to prefer zip
is that if you tried using the same code with a different Collection (e.g. Set) you could get the following syntax error when indexing into the type (items[index]
):
Cannot convert value of type 'Int' to expected argument type 'Set.Index'
By using the zip
based approach, you can still index into the collection.
You could also create an extension on collection if you plan on using it often.
You can test this all out in a Playground:
import PlaygroundSupport import SwiftUI // MARK: - Array let array = ["a", "b", "c"] Array(array.enumerated()) // [(offset 0, element "a"), (offset 1, element "b"), (offset 2, element "c")] Array(zip(array.indices, array)) // [(.0 0, .1 "a"), (.0 1, .1 "b"), (.0 2, .1 "c")] let arrayView = Group { ForEach(Array(array.enumerated()), id: \.offset) { offset, element in PrintView("offset: \(offset), element: \(element)") Text("value: \(array[offset])") } // offset: 0, element: a // offset: 1, element: b // offset: 2, element: c ForEach(Array(zip(array.indices, array)), id: \.0) { index, element in PrintView("index: \(index), element: \(element)") Text("value: \(array[index])") } // index: 0, element: a // index: 1, element: b // index: 2, element: c } // MARK: - Array Slice let arraySlice = array[1...2] // ["b", "c"] Array(arraySlice.enumerated()) // [(offset 0, element "b"), (offset 1, element "c")] Array(zip(arraySlice.indices, arraySlice)) // [(.0 1, .1 "b"), (.0 2, .1 "c")] // arraySlice[0] // ❌ EXC_BAD_INSTRUCTION arraySlice[1] // "b" arraySlice[2] // "c" let arraySliceView = Group { ForEach(Array(arraySlice.enumerated()), id: \.offset) { offset, element in PrintView("offset: \(offset), element: \(element)") // Text("value: \(arraySlice[offset])") ❌ Fatal error: Index out of bounds } // offset: 0, element: b // offset: 1, element: c ForEach(Array(zip(arraySlice.indices, arraySlice)), id: \.0) { index, element in PrintView("index: \(index), element: \(element)") Text("value: \(arraySlice[index])") } // index: 1, element: b // index: 2, element: c } // MARK: - Set let set: Set = ["a", "b", "c"] Array(set.enumerated()) // [(offset 0, element "b"), (offset 1, element "c"), (offset 2, element "a")] Array(zip(set.indices, set)) // [({…}, .1 "a"), ({…}, .1 "b"), ({…}, .1 "c")] let setView = Group { ForEach(Array(set.enumerated()), id: \.offset) { offset, element in PrintView("offset: \(offset), element: \(element)") // Text("value: \(set[offset])") // ❌ Syntax error: Cannot convert value of type 'Int' to expected argument type 'Set<String>.Index' } // offset: 0, element: a // offset: 1, element: b // offset: 2, element: c ForEach(Array(zip(set.indices, set)), id: \.0) { index, element in PrintView("index: \(index), element: \(element)") Text("value: \(set[index])") } // index: Index(_variant: Swift.Set<Swift.String>.Index._Variant.native(Swift._HashTable.Index(bucket: Swift._HashTable.Bucket(offset: 0), age: -481854246))), element: a // index: Index(_variant: Swift.Set<Swift.String>.Index._Variant.native(Swift._HashTable.Index(bucket: Swift._HashTable.Bucket(offset: 2), age: -481854246))), element: b // index: Index(_variant: Swift.Set<Swift.String>.Index._Variant.native(Swift._HashTable.Index(bucket: Swift._HashTable.Bucket(offset: 3), age: -481854246))), element: c } // MARK: - struct PrintView: View { init(_ string: String) { print(string) self.string = string } var string: String var body: some View { Text(string) } } let allViews = Group { arrayView arraySliceView setView } PlaygroundPage.current.setLiveView(allViews)
When you enumerate this collection, each element in the enumeration is a tuple of type:
(offset: Int, element: Int)
so the id param should be changed from id: \.self
to id: \.element
.
ForEach(items.enumerated(), id: \.element) { ...
However after this change you'll still get the error:
Referencing initializer 'init(_:id:content:)' on 'ForEach' requires that 'EnumeratedSequence<[Int]>' conform to 'RandomAccessCollection'
because ForEach
requires random access to the data, but an Enumeration only allows in-order access. To fix this, convert the enumeration to an array.
ForEach(Array(items.enumerated()), id: \.element) { ...
Here's an extension you can use to make this a little easier:
extension Collection { func enumeratedArray() -> Array<(offset: Int, element: Self.Element)> { return Array(self.enumerated()) } }
and an example that can be run in a (macos) Xcode playground:
import AppKit import PlaygroundSupport import SwiftUI extension Collection { func enumeratedArray() -> Array<(offset: Int, element: Self.Element)> { return Array(self.enumerated()) } } struct App: View { let items = 100...200 var body: some View { List { ForEach(items.enumeratedArray(), id: \.element) { index, item in Text("\(index): Item \(item)") } }.frame(width: 200, height: 200) } } PlaygroundPage.current.liveView = NSHostingView(rootView: App())
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With