Get index in ForEach in SwiftUI
I have an array and I want to iterate through it initialize views based on array value, and want to perform action based on array item index
When I iterate through objects
ForEach(array, id: \.self) { item in
CustomView(item: item)
.tapAction {
self.doSomething(index) // Can't get index, so this won't work
}
}
So, I've tried another approach
ForEach((0..<array.count)) { index in
CustomView(item: array[index])
.tapAction {
self.doSomething(index)
}
}
But the issue with second approach is, that when I change array, for example, if doSomething
does following
self.array = [1,2,3]
views in ForEach
do not change, even if values are changed. I believe, that happens because array.count
haven't changed.
Is there a solution for this? Thanks in advance.
This works for me:
Using Range and Count
struct ContentView: View {
@State private var array = [1, 1, 2]
func doSomething(index: Int) {
self.array = [1, 2, 3]
}
var body: some View {
ForEach(0..<array.count) { i in
Text("\(self.array[i])")
.onTapGesture { self.doSomething(index: i) }
}
}
}
Using Array's Indices
The indices
property is a range of numbers.
struct ContentView: View {
@State private var array = [1, 1, 2]
func doSomething(index: Int) {
self.array = [1, 2, 3]
}
var body: some View {
ForEach(array.indices) { i in
Text("\(self.array[i])")
.onTapGesture { self.doSomething(index: i) }
}
}
}
Another approach is to use:
enumerated()
ForEach(Array(array.enumerated()), id: \.offset) { index, element in
// ...
}
Source: https://alejandromp.com/blog/swiftui-enumerated/
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'scontent
closure is not a@ViewBuilder
.
The only change from that is theid
parameter is visible and can be changed