【问题标题】:Get index in ForEach in SwiftUI在 SwiftUI 中的 ForEach 中获取索引
【发布时间】:2019-12-06 06:07:24
【问题描述】:

我有一个数组,我想遍历它根据数组值初始化视图,并希望根据数组项索引执行操作

当我遍历对象时

ForEach(array, id: \.self) { item in
  CustomView(item: item)
    .tapAction {
      self.doSomething(index) // Can't get index, so this won't work
    }
}

所以,我尝试了另一种方法

ForEach((0..<array.count)) { index in
  CustomView(item: array[index])
    .tapAction {
      self.doSomething(index)
    }
}

但是第二种方法的问题是,当我更改数组时,例如,如果doSomething 确实跟随

self.array = [1,2,3]

ForEach 中的视图不会更改,即使值已更改。我相信,这是因为array.count 没有改变。

有解决办法吗?提前致谢。

【问题讨论】:

    标签: swift swiftui


    【解决方案1】:

    这对我有用:

    使用范围和计数

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

    使用数组的索引

    indices 属性是一个数字范围。

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

    【讨论】:

    • 我相信,如果数组中有相同的元素,array.firstIndex(of: item) 将不起作用,例如如果数组是[1,1,1],它将为所有元素返回0。此外,对数组的每个元素执行此操作也不是很好的性能。我的数组是一个@State 变量。 @State private var array: [Int] = [1, 1, 2]
    • 好的,我更新了我的答案。如果数组是@State,则第二种方法对我来说很好。
    • 永远不要使用0..&lt;array.count。请改用array.indicesgithub.com/amomchilov/Blog/blob/master/…
    • 请注意the iOS 13 beta 5 release notes 给出以下关于将范围传递给ForEach 的警告:“但是,您不应传递在运行时更改的范围。如果您使用在运行时更改的变量来定义范围,则列表会根据初始范围显示视图并忽略对该范围的任何后续更新。”
    • 谢谢你,Rob,这解释了上述产生的数组索引越界错误 - 我的数组的内容会随着时间而变化。有问题,因为这基本上使我的解决方案无效。
    【解决方案2】:

    另一种方法是使用:

    枚举()

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

    来源:https://alejandromp.com/blog/swiftui-enumerated/

    【讨论】:

    • 我认为您希望 \.element 确保动画正常工作。
    • ForEach(Array(array.enumerated()), id: \.1) { index, element in ... }
    • \.1 是什么意思?
    • @Luca Array(array.enumerated()) 实际上有EnumeratedSequence&lt;[ItemType]&gt; 类型,它的元素是(offset: Int, element: ItemType) 类型的元组。编写\.1 意味着您访问元组的第一个(人类语言 - 第二个)元素,即 - element: ItemType。这与写 \.element 相同,但更短且 IMO 更难理解
    • 使用 \.element 的另一个好处是 swiftUI 能够确定数组的内容何时发生了变化,即使大小可能没有变化。使用 \.element!
    【解决方案3】:

    我需要一个更通用的解决方案,它可以处理所有类型的数据(实现 RandomAccessCollection),并且还可以通过使用范围来防止未定义的行为。
    我最终得到以下结果:

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

    这样,问题中的原始代码就可以替换为:

    ForEachWithIndex(array, id: \.self) { index, item in
      CustomView(item: item)
        .tapAction {
          self.doSomething(index) // Now works
        }
    }
    

    获取索引以及元素。

    请注意,API 与 SwiftUI 的 API 是镜像的 - 这意味着带有 id 参数的 content 闭包的初始化程序不是 @ViewBuilder
    唯一的变化是 id 参数可见并且可以更改

    【讨论】:

    • 这很棒。一个小建议 - 如果您公开数据,那么您也可以遵守 DynamicViewContent(免费),它可以使 .onDelete() 等修饰符起作用。
    • @ConfusedVorlon 哦,确实!我已将答案更新为公开datacontent,就像原始ForEach 的情况一样(我保留id 原样),并将一致性添加到DynamicViewContent。感谢您的建议!
    • 感谢您的更新。不确定它是否重要 - 但 ForEach 稍微限制了一致性扩展 ForEach : DynamicViewContent where Content : View {}
    • @ConfusedVorlon 我也不确定,如果不了解这里的内部工作原理,很难说。我会添加它以防万一;我认为没有View 的人不会使用它作为Content 哈哈
    • 一件小事:datacontentid 可以声明为常量。
    【解决方案4】:

    对于基于非零的数组,避免使用枚举,而是使用 zip:

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

    【讨论】:

      【解决方案5】:

      我为此创建了一个专用的View,基于真棒斯通的解决方案:

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

      现在你可以像这样使用它了:

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

      【讨论】:

        【解决方案6】:

        我通常使用enumerated 来获得一对indexelement,其中elementid

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

        更多复用的组件,可以访问这篇文章https://onmyway133.com/posts/how-to-use-foreach-with-indices-in-swiftui/

        【讨论】:

          【解决方案7】:

          ForEach is SwiftUI 与 for 循环不同,它实际上是在生成一种称为结构标识的东西。 ForEach 的文档说明:

          /// 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.
          

          这意味着我们不能在 ForEach 中使用索引、枚举或新数组。 ForEach 必须在可识别项目的实际数组上。这样 SwiftUI 可以跟踪移动的行视图。

          要解决获取索引的问题,您只需像这样查找索引:

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

          您可以在他们的Scrumdinger sample app tutorial 中看到 Apple 这样做。

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

          【讨论】:

            【解决方案8】:

            以下方法的优点是,即使状态值发生变化,ForEach 中的视图也会发生变化:

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

            ...这也可以用于,例如,删除最后一个分隔符 在列表中:

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

            【讨论】:

              【解决方案9】:

              这是一个简单的解决方案,但对上面的解决方案来说效率很低..

              在您的点按操作中,通过您的项目

              .tapAction {
              
                 var index = self.getPosition(item)
              
              }
              

              然后创建一个函数,通过比较 id 来查找该项目的索引

              func getPosition(item: Item) -> Int {
              
                for i in 0..<array.count {
                      
                      if (array[i].id == item.id){
                          return i
                      }
                      
                  }
                  
                  return 0
              }
              

              【讨论】:

                【解决方案10】:

                2021 解决方案如果您使用基于非零的数组,请避免使用枚举:

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

                【讨论】:

                  猜你喜欢
                  • 1970-01-01
                  • 1970-01-01
                  • 1970-01-01
                  • 1970-01-01
                  • 1970-01-01
                  • 1970-01-01
                  • 1970-01-01
                  • 2021-09-28
                  • 2020-12-09
                  相关资源
                  最近更新 更多