Get index in ForEach in SwiftUI

前端 未结 6 1889
执笔经年
执笔经年 2020-12-24 05:01

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 o

相关标签:
6条回答
  • 2020-12-24 05:30

    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

    0 讨论(0)
  • 2020-12-24 05:34

    Another approach is to use:

    enumerated()

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

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

    0 讨论(0)
  • 2020-12-24 05:46

    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()
                    }
                }
            }
        }
    }
    
    0 讨论(0)
  • 2020-12-24 05:49

    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
        ...
    }
    
    0 讨论(0)
  • 2020-12-24 05:50

    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) }
            }
        }
    }
    
    0 讨论(0)
  • 2020-12-24 05:53

    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
    }
    
    0 讨论(0)
提交回复
热议问题