I needed a more generic solution, that could work on all kind of data (that implements RandomAccessCollection), and also prevent undefined behavior by using ranges.
I ended up with the following:
public struct ForEachWithIndex<Data: RandomAccessCollection, ID: Hashable, Content: View>: View { public var data: Data public var content: (_ index: Data.Index, _ element: Data.Element) -> Content var id: KeyPath<Data.Element, ID> public init(_ data: Data, id: KeyPath<Data.Element, ID>, content: @escaping (_ index: Data.Index, _ element: Data.Element) -> Content) { self.data = data self.id = id self.content = content } public var body: some View { ForEach( zip(self.data.indices, self.data).map { index, element in IndexInfo( index: index, id: self.id, element: element ) }, id: \.elementID ) { indexInfo in self.content(indexInfo.index, indexInfo.element) } } } extension ForEachWithIndex where ID == Data.Element.ID, Content: View, Data.Element: Identifiable { public init(_ data: Data, @ViewBuilder content: @escaping (_ index: Data.Index, _ element: Data.Element) -> Content) { self.init(data, id: \.id, content: content) } } extension ForEachWithIndex: DynamicViewContent where Content: View { } private struct IndexInfo<Index, Element, ID: Hashable>: Hashable { let index: Index let id: KeyPath<Element, ID> let element: Element var elementID: ID { self.element[keyPath: self.id] } static func == (_ lhs: IndexInfo, _ rhs: IndexInfo) -> Bool { lhs.elementID == rhs.elementID } func hash(into hasher: inout Hasher) { self.elementID.hash(into: &hasher) } }
This way, the original code in the question can just be replaced by:
ForEachWithIndex(array, id: \.self) { index, item in CustomView(item: item) .tapAction { self.doSomething(index) // Now works } }
To get the index as well as the element.
Note that the API is mirrored to that of SwiftUI - this means that the initializer with the id parameter's content closure is not a @ViewBuilder.
The only change from that is the id parameter is visible and can be changed