93

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?

6 Answers 6

180

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
}

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 probably zip-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)

Updates:

  • Deleted the part that mentioned you can use \.1 since Peacemoon points out this could cause problems. Also I'm pretty sure if your items conform to Identifiable, there's no point in using zip in the first place, you should be able to just do ForEach(identifiableItems).
Sign up to request clarification or add additional context in comments.

3 Comments

Be careful to follow (You can also use id: \.1 if your items conform to Identifiable.) . If you use \.1. the index seems to be wrong. For me it's always 1
@Senseful, could you explain what id: \.0 means here?
It refers to the array of indices. In this context, the code is using the array's indices (which are always unique) as unique keys for each element in the ForEach
46

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())

5 Comments

I got the following error Type '(offset: Int, element: ProductCategory)' cannot conform to 'Hashable'; only struct/enum/class types can conform to protocols I have made sure that my custom class ProductCategory conforms to hashable and can be looped with foreach normally. what am I missing?
If your array item has an id property, then in the id: argument of the ForEach, insert: id: \.element.id. You could also try \.element.self
Warning: Relying on enumerated() in production could lead to crashes with certain collection types. You should use zip instead.
@Senseful It only leads to crashes if you try to index into the collection using the offset. If you're only using the offset as the id in the ForEach (which is what most of my use cases are), then using `enumerated() is perfectly fine.
@PeterSchorn: Correct, in this (and in most cases) you're fine. However, if you refactor the code months later, you might accidentally use the index and it could lead to a crash. I prefer to write defensive code that doesn't require me to remember all this context.
19

One of the Apple SwiftUI examples enumerated() is using inside Array, and then you can add offset to be id, which is unique when you make Array enumerated.

ForEach(Array(data.enumerated()), id: \.offset) { index, observation in

2 Comments

This is actually the best answer because then I don't have to make my struct conform to Identifiable. Thanks!
Best practice is to make each view in a ForEach conform to Identifiable. You could have problems with animations (among other issues) if you do it this way.
17

In most cases, you don't need to enumerate it as it's kind of slow.

struct App: View {
    let items = Array(100...200)
    var body: some View {
        List {
           ForEach(items.indices, id: \.self) { index in
               Text("Item \(self.items[index])")
           }
        }.id(items).frame(width: 200, height: 200)
    }
}

3 Comments

Nice answer, but can you elaborate on enumerated being slow? It's documented complexity is O(1). Also this approach results in the indices being used as the id which works fine in this simple case, but doesn't work when you're re-ordering Identifiable objects in a List.
If you need to trigger the items change, just add the .id() for the list.
That can work in certain situations, but not if you want your changes to be properly animated.
3

If you want to re-use @senseful's answer you can do it like this:

struct ForEachIndexed<Data, Item, Content: View>: View where Data: RandomAccessCollection<Item>, Data.Index: Hashable {
    private let sequence: Data
    private let content: (Data.Index, Item) -> Content

    init(_ sequence: Data, @ViewBuilder _ content: @escaping (Data.Index, Item) -> Content) {
        self.sequence = sequence
        self.content = content
    }

    var body: some View {
        ForEach(Array(zip(sequence.indices, sequence)), id: \.0) { index, item in
            self.content(index, item)
        }
    }
}

And then use it like so:

ForEachIndexed([11, 17, 31]) { index, item in
    Text("\(index): \(item)")
}

Comments

0

I've created a SwiftUI ForEach extension that does just that:

extension ForEach where Content: View {
    init<C: RandomAccessCollection>(enumerating data: C, id: KeyPath<(offset: Int, element: C.Element), ID>, @ViewBuilder content: @escaping (Int, C.Element) -> Content) where Data == Array<(offset: Int, element: C.Element)> {
        self.init(Array(data.enumerated()), id: id, content: content)
    }
}

You can use it like this:

let items = [...]

ForEach(enumerating: items, id: \.offset) { offset, item in
    ...
}

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.