Get index in ForEach in SwiftUI

SwiftSwiftui

Swift Problem Overview


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.

Swift Solutions


Solution 1 - Swift

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

Solution 2 - Swift

Another approach is to use:

enumerated()

ForEach(Array(array.enumerated()), id: \.offset) { index, element in
  // ...
}

Source: https://alejandromp.com/blog/swiftui-enumerated/

Solution 3 - Swift

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

Solution 4 - Swift

I usually use enumerated to get a pair of index and element with the element as the id

ForEach(Array(array.enumerated()), id: \.element) { index, element in
    Text("\(index)")
    Text(element.description)
}

For a more reusable component, you can visit this article https://onmyway133.com/posts/how-to-use-foreach-with-indices-in-swiftui/

Solution 5 - Swift

For non zero based arrays avoid using enumerated, instead use zip:

ForEach(Array(zip(items.indices, items)), id: \.0) { index, item in
  // Add Code here
}

Solution 6 - Swift

I created a dedicated View for this purpose based on the awesome Stone's solution:

struct EnumeratedForEach<ItemType, ContentView: View>: View {
	let data: [ItemType]
	let content: (Int, ItemType) -> ContentView
	
	init(_ data: [ItemType], @ViewBuilder content: @escaping (Int, ItemType) -> ContentView) {
		self.data = data
		self.content = content
	}
	
	var body: some View {
		ForEach(Array(self.data.enumerated()), id: \.offset) { idx, item in
			self.content(idx, item)
		}
	}
}

Now you can use it like this:

EnumeratedForEach(items) { idx, item in
	...
}

Solution 7 - Swift

The advantage of the following approach is that the views in ForEach even change if state values ​​change:

struct ContentView: View {
    @State private var array = [1, 2, 3]
    
    func doSomething(index: Int) {
        self.array[index] = Int.random(in: 1..<100)
    }
    
    var body: some View {    
        let arrayIndexed = array.enumerated().map({ $0 })
        
        return List(arrayIndexed, id: \.element) { index, item in
            
            Text("\(item)")
                .padding(20)
                .background(Color.green)
                .onTapGesture {
                    self.doSomething(index: index)
            }
        }
    }
}

> ... this can also be used, for example, to remove the last divider > in a list:

struct ContentView: View {
    
    init() {
        UITableView.appearance().separatorStyle = .none
    }
    
    var body: some View {
        let arrayIndexed = [Int](1...5).enumerated().map({ $0 })

        return List(arrayIndexed, id: \.element) { index, number in
            
            VStack(alignment: .leading) {
                Text("\(number)")
                
                if index < arrayIndexed.count - 1 {
                    Divider()
                }
            }
        }
    }
}

Solution 8 - Swift

2021 solution if you use non zero based arrays avoid using enumerated:

ForEach(array.indices,id:\.self) { index in
    VStack {
        Text(array[index].name)
            .customFont(name: "STC", style: .headline)
            .foregroundColor(Color.themeTitle)
        }
    }
}

Solution 9 - Swift

ForEach is SwiftUI isn’t the same as a for loop, it’s actually doing generating something called structural identity. The documentation of ForEach states:

/// It's important that the `id` of a data element doesn't change, unless
/// SwiftUI considers the data element to have been replaced with a new data
/// element that has a new identity.

This means we cannot use indices, enumerated or a new Array in the ForEach. The ForEach must be on the actual array of identifiable items. This is so SwiftUI can track the row Views moving around.

To solve your problem of getting the index, you simply have to look up the index like this:

ForEach(items) { item in
  CustomView(item: item)
    .tapAction {
      if let index = array.firstIndex(where: { $0.id == item.id }) {
          self.doSomething(index) 
      }
    }
}

You can see Apple doing this in their Scrumdinger sample app tutorial.

guard let scrumIndex = scrums.firstIndex(where: { $0.id == scrum.id }) else {
            fatalError("Can't find scrum in array")
        }

Solution 10 - Swift

Here is a simple solution though quite inefficient to the ones above..

In your Tap Action, pass through your item

.tapAction {

   var index = self.getPosition(item)

}

Then create a function the finds the index of that item by comparing the id

func getPosition(item: Item) -> Int {

  for i in 0..<array.count {
        
        if (array[i].id == item.id){
            return i
        }
        
    }
    
    return 0
}

Solution 11 - Swift

Just like they mentioned you can use array.indices for this purpose BUT remember that indexes that you've got are started from last element of array, To fix this issue you must use this: array.indices.reversed() also you should provide an id for the ForEach. Here's an example:

ForEach(array.indices.reversed(), id:\.self) { index in }

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionPavelView Question on Stackoverflow
Solution 1 - SwiftkontikiView Answer on Stackoverflow
Solution 2 - SwiftStoneView Answer on Stackoverflow
Solution 3 - SwiftStéphane CopinView Answer on Stackoverflow
Solution 4 - Swiftonmyway133View Answer on Stackoverflow
Solution 5 - SwiftAndrew StoddartView Answer on Stackoverflow
Solution 6 - SwiftramzesenokView Answer on Stackoverflow
Solution 7 - SwiftPeter KreinzView Answer on Stackoverflow
Solution 8 - SwiftMohamed AyedView Answer on Stackoverflow
Solution 9 - SwiftmalhalView Answer on Stackoverflow
Solution 10 - SwiftC. SkjerdalView Answer on Stackoverflow
Solution 11 - SwiftMehdiView Answer on Stackoverflow