243

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?

14 Answers 14

267

Another approach is to use:

enumerated()

Update for Swift 6.2:

Swift 6.2 introduces Collection conformances for enumerated(). (SE-0459)

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

Before Swift 6.2:

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

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

Sign up to request clarification or add additional context in comments.

8 Comments

I think you want \.element to ensure animations work properly.
ForEach(Array(array.enumerated()), id: \.1) { index, element in ... }
@Luca Array(array.enumerated()) has actually EnumeratedSequence<[ItemType]> type and its element is a tuple of type (offset: Int, element: ItemType). Writing \.1 means you access the 1st (in human language - 2nd) element of tuple, namely - element: ItemType. This is the same as writing \.element, but shorter and IMO much less understandable
Using \.element has the added benefit of swiftUI being able to determine when the contents of the array has changed, even though the size may have not. Use \.element!
Yes, use \.element as @ramzesenok mentioned and implement Hashable protocol if you are using a custom object array, and add the properties that could change/update inside your object inside func hash(into hasher: inout Hasher) { and there add for example in my case status update like status.hash(into: &hasher)
|
142

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

10 Comments

I believe, that array.firstIndex(of: item) won't work if there are same elements in array, for example if array is [1,1,1], it will return 0 for all elements. Also, it is not very good performance to do this for each element of array. my array is a @State variable. @State private var array: [Int] = [1, 1, 2]
Never use 0..<array.count. Use array.indices instead. github.com/amomchilov/Blog/blob/master/…
Please note that the iOS 13 beta 5 release notes give the following warning about passing a range to ForEach: “However, you shouldn’t pass a range that changes at runtime. If you use a variable that changes at runtime to define the range, the list displays views according to the initial range and ignores any subsequent updates to the range.”
Thank you, Rob, that explains the array index out of bounds error generated by the above - my array's content changes over time. Problematic, as that essentially invalidates this solution for me.
developer.apple.com/videos/play/wwdc2021/10022 Using Index is not ideal if array content position can change like adding or removing content. As index is being used for implicit identification of views. We should explicitly set id modifier and use consistent id for same view
|
75

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/

1 Comment

Using 'element' doesn't work for me. If I give it an array of 10 (Identifiable, Hashable) items, it only reports the indices of the first two elements of the array. It only works properly if I use 'offset' instead of 'element'. But I haven't tested animation yet...
32

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

5 Comments

This is great. One minor suggestion - if you make data public, then you can also conform to DynamicViewContent (for free) which enables modifiers like .onDelete() to work.
@ConfusedVorlon oh indeed! I've updated the answer to have data & content public, as is the case for the original ForEach (I left id as it is), and added the conformance to DynamicViewContent. Thanks for the suggestion!
Thanks for the update. Not sure if it is important -but ForEach limits the conformance slightly extension ForEach : DynamicViewContent where Content : View {}
@ConfusedVorlon I'm not sure either, it's tough to say without knowing the inner workings here. I'll add it just in case; I don't think anyone would use this without a View as the Content haha
Just a minor thing: data, content and id can be declared as constants.
31

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
}

3 Comments

Out of curiosity, why don't you use a zero-based array? What's the context?
For example, an ArraySlice's indices will be the indices of its items in its parent array, so they may not start from 0.
By using the index (\.0) as the id, you're not providing a consistent identity for the item, which will lead to view inconsistencies if items changes.
29

I created a dedicated View for this purpose:

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(zip(data.indices, data)), id: \.0) { idx, item in
            content(idx, item)
        }
    }
}

Now you can use it like this:

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

As mentioned in comments by Robin Daugherty it's not a good practice to identify elements by their index in the array – it's fine sometimes but not when you want to reorder or insert/remove elements. Therefore if you want to do it right – conform ItemType to Identifiable (it's up to you then to identify each element properly) and use the element itself in the ForEach as an id, like this (notice id: \.1 instead of id: \.0):

ForEach(Array(zip(data.indices, data)), id: \.1) { idx, item in
    content(idx, item)
}


Small note: \.0 and \.1 correspond to element from the first array you pass to zip and element from the second array respectively. It's coming from the tuple from array of tuples, created when you write Array(zip(..., ...)).

In this example since we pass data.indices as the first array, \.0 is index and \.1 is the element itself, since the second array we pass is just array of elements – data

3 Comments

This answer also uses the array index as the item identifier, which is bad practice. ItemType should conform to Identifiable and id should be \.1, referring to the element, not the index. See the answer by @onmyway133
thanks, you're right. I've updated the post and added more explanation
Thank you! I also conformed the view to DynamicViewContent to allow for things like .onMove { }
24

You can use this method:

.enumerated()

From the Swift documentation:

Returns a sequence of pairs (n, x), where n represents a consecutive integer starting at zero and x represents an element of the sequence.

var elements: [String] = ["element 1", "element 2", "element 3", "element 4"]

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

Comments

16

ForEach is SwiftUI isn’t the same as a for loop, it’s actually doing 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 given the actual array of identifiable items. This is so SwiftUI can animate the rows around to match the data, obviously this can't work with indicies, e.g. if row at 0 is moved to 1 its index is still 0.

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

3 Comments

this will lead to sluggishness when your have lots of rows.
While this does work, I've found it increases your chances of running into the dreaded "compiler is unable to type-check this expression in reasonable time" error. onmyway133 has a great answer below on how to do this well
if you don’t use item IDs then you can’t modify the array
9

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

1 Comment

This should be the accepted answer. List is lazy, unlike ForEach, and this approach retains this behavior. Print the index inside .onAppear on a cell to see the difference
6

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

3 Comments

This is a bad idea because it replaces the Identifiable object with the index, which means that SwiftUI won't be able to detect correctly when things change vs move.
@RobinDaughert how to avoid that if I still want to use ForEach with indices, because I got problem you mentioned
Both @onmyway's answer (which is the accepted answer) and @malhal's answer can give you a better method. If you must, you can use the id function to place a meaningful identity on the view, but that's not necessary if you use ForEach correctly, as Apple recommends.
5

To get indexing from SwiftUI's ForEach loop, you could use closure's shorthand argument names:

@State private var cars = ["Aurus","Bentley","Cadillac","Genesis"]

var body: some View {
    NavigationView {
        List {
            ForEach(Array(cars.enumerated()), id: \.offset) {

                Text("\($0.element) at \($0.offset) index")
            }
        }
    }
}

Results:

//   Aurus at 0 index
//   Bentley at 1 index
//   Cadillac at 2 index
//   Genesis at 3 index


P. S.

Initially, I posted an answer with a "common" expression that all Swift developers are used to, however, thanks to @loremipsum I changed it. As stated in WWDC 2021 Demystify SwiftUI video (time 33:40), array indices are not stable from \.self identity (key path).

ForEach(0 ..< cars.count, id: \.self) {     // – NOT STABLE
    Text("\(cars[$0]) at \($0) index")
}

5 Comments

This is considered bad practice and unsafe by apple when using SwiftUI you can watch Demystifying SwiftUI from one of the WWDCs for more info.
Using count and the index. All the index versions of this answer are considered unsafe. The one with enumerated is the most acceptable version.
Using enumerated() doesn't change anything if you still use \.offset as the id. You need to use \.element.
@RobinDaugherty \.element doesn't work properly either. It doesn't provide access to all the indices in the given array.
@Womble The Foreach id argument should refer to the identity of the items being enumerated, not their offset in the array. Using \.offset as the id is not a stable identifier. @Andy Jazz both of your code snippets have the same issue because they're essentially the same. In any "real" scenario the item being enumerated should conform to Identifiable. In this case the item is a string, so \.element would be a stable identifier, as long as the array will never contain duplicates. See the answer by @onmyway133
3

I use extension ForEach to resolve this issue:


extension ForEach {
  
  public init<T: RandomAccessCollection>(
    data: T,
    content: @escaping (T.Index, T.Element) -> Content
  ) where T.Element: Identifiable, T.Element: Hashable, Content: View, Data == [(T.Index, T.Element)], ID == T.Element  {
    self.init(Array(zip(data.indices, data)), id: \.1) { index, element in
      content(index, element)
    }
  }
}

How to using:

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

Comments

2

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
}

Comments

-3

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 }

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.