在SwiftUI中获取ForEach中的索引

Pav*_*vel 7 swift swiftui

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

当我遍历对象时

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

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

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

但随着第二种方法的问题是,当我改变阵列,例如,如果doSomething做以下

self.array = [1,2,3]
Run Code Online (Sandbox Code Playgroud)

ForEach即使值已更改,视图也不会更改。我相信,发生这种情况是因为array.count没有改变。

有解决方案吗?提前致谢。

onm*_*133 66

我通常会买enumerated一对indexelement作为elementid

ForEach(Array(array.enumerated()), id: \.element) { index, element in
    Text("\(index)")
    Text(element.description)
}
Run Code Online (Sandbox Code Playgroud)

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


Sto*_*one 51

另一种方法是使用:

枚举()

ForEach(Array(array.enumerated()), id: \.offset) { index, element in
  // ...
}
Run Code Online (Sandbox Code Playgroud)

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

  • @Luca `Array(array.enumerated())` 实际上具有 `EnumeratedSequence&lt;[ItemType]&gt;` 类型,其元素是类型为 `(offset: Int, element: ItemType)` 的元组。编写 `\.1` 意味着您访问元组的第一个(在人类语言中 - 第二个)元素,即 - `element: ItemType`。这与编写“\.element”相同,但更短并且在我看来更难理解 (9认同)
  • 我认为你需要 `\.element` 来确保动画正常工作。 (5认同)
  • 使用 \.element 的另一个好处是 swiftUI 能够确定数组内容何时更改,即使大小可能没有更改。使用 \.element! (4认同)
  • ForEach(Array(array.enumerated()), id: \.1) { 索引, 元素在... } (3认同)
  • 是的,如果您使用自定义对象数组,请使用 @ramzesenok 提到的 `\.element` 并实现 `Hashable` 协议,并在 `func hash(into hasher: inout Hasher) 内添加可以在对象内部更改/更新的属性{` 例如,在我的案例中添加状态更新,如 `status.hash(into: &amp;hasher)` (3认同)
  • \.1 是什么意思? (2认同)
  • 如果元素类型已经符合 Identificate,您可能需要使用 `id: \.element.id`。如果您使用“id: \.element”,则元素类型将需要符合 Hashable。 (2认同)

Sté*_*pin 18

我需要一个更通用的解决方案,它可以处理所有类型的数据(实现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)
    }
}
Run Code Online (Sandbox Code Playgroud)

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

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

获取索引以及元素。

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


mal*_*hal 15

ForEachSwiftUI 与 for 循环不同,它实际上在做一些称为结构同一性的事情。状态的文档ForEach

\n
/// It\'s important that the `id` of a data element doesn\'t change, unless\n/// SwiftUI considers the data element to have been replaced with a new data\n/// element that has a new identity.\n
Run Code Online (Sandbox Code Playgroud)\n

这意味着我们不能在ForEach. 必须ForEach给出可识别项目的实际数组。这样 SwiftUI 就可以对行进行动画处理以匹配数据,显然这不能与索引一起使用,例如,如果 0 处的行移动到 1,其索引仍然是 0。

\n

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

\n
ForEach(items) { item in\n  CustomView(item: item)\n    .tapAction {\n      if let index = array.firstIndex(where: { $0.id == item.id }) {\n          self.doSomething(index) \n      }\n    }\n}\n
Run Code Online (Sandbox Code Playgroud)\n

您可以在其Scrumdinger 示例应用程序教程中看到 Apple 这样做。

\n
guard let scrumIndex = scrums.firstIndex(where: { $0.id == scrum.id }) else {\n    fatalError("Can\'t find scrum in array")\n}\n
Run Code Online (Sandbox Code Playgroud)\n


小智 11

你可以使用这个方法:

.enumerated()

来自 Swift 文档:

返回 (n, x) 对的序列,其中 n 表示从零开始的连续整数,x 表示序列的元素。

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

ForEach(Array(elements.enumerated()), id: \.element) { index, element in
  Text("\(index) \(element)")
}
Run Code Online (Sandbox Code Playgroud)


Pet*_*inz 9

以下方法的优点是,如果状态值 \xe2\x80\x8b\xe2\x80\x8b 更改,ForEach 中的视图甚至会更改:

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

...这也可以用于删除列表中的最后一个分隔符\n:

\n
\n\n
struct ContentView: View {\n\n    init() {\n        UITableView.appearance().separatorStyle = .none\n    }\n\n    var body: some View {\n        let arrayIndexed = [Int](1...5).enumerated().map({ $0 })\n\n        return List(arrayIndexed, id: \\.element) { index, number in\n\n            VStack(alignment: .leading) {\n                Text("\\(number)")\n\n                if index < arrayIndexed.count - 1 {\n                    Divider()\n                }\n            }\n        }\n    }\n}\n
Run Code Online (Sandbox Code Playgroud)\n


And*_*art 9

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

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

  • 通过使用索引 (`\.0`) 作为 id,您没有为项目提供一致的标识,如果 `items` 发生更改,这将导致视图不一致。 (3认同)
  • 例如,“ArraySlice”的索引将是其父数组中项目的索引,因此它们可能不会从 0 开始。 (2认同)

kon*_*iki 7

这对我有用:

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])")
            .tapAction { self.doSomething(index: i) }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

  • 切勿使用0 .. &lt;array.count`。使用`array.indices`代替。https://github.com/amomchilov/Blog/blob/master/Proper%20Array%20Iteration.md (6认同)
  • https://developer.apple.com/videos/play/wwdc2021/10022/ 如果数组内容位置可以更改(例如添加或删除内容),则使用索引并不理想。因为索引被用于视图的隐式识别。我们应该显式设置 id 修饰符并为同一视图使用一致的 id (4认同)
  • 谢谢你,Rob,这解释了上面生成的数组索引越界错误 - 我的数组的内容随着时间的推移而变化。有问题,因为这本质上使我的解决方案无效。 (3认同)
  • 我相信,如果array中有相同的元素,则array.firstIndex(of:item)不会起作用,例如,如果array为`[1,1,1]`,它将为所有元素返回`0`。 。同样,对数组的每个元素执行此操作也不是很好的性能。我的数组是一个@State变量。`@State私有var数组:[Int] = [1、1、2] (2认同)
  • 请注意,[iOS 13 beta 5发行说明](https://developer.apple.com/documentation/ios_ipados_release_notes/ios_ipados_13_beta_5_release_notes?language=objc)发出以下有关将范围传递给`ForEach`的警告:不应传递在运行时更改的范围。如果您使用在运行时更改的变量来定义范围,则列表将根据初始范围显示视图,并忽略对该范围的任何后续更新。” (2认同)

小智 6

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

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

  • 这是一个坏主意,因为它用索引替换了“可识别”对象,这意味着 SwiftUI 将无法正确检测事物何时发生变化或移动。 (2认同)

ram*_*nok 5

View基于令人敬畏的 Stone 的解决方案为此目的创建了一个专用的:

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)
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

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

EnumeratedForEach(items) { idx, item in
    ...
}
Run Code Online (Sandbox Code Playgroud)


ARG*_*Geo 5

要从 SwiftUI 的 ForEach 循环获取索引,您可以使用闭包的简写参数名称:

\n
@State private var cars = ["Aurus","Bentley","Cadillac","Genesis"]\n\nvar body: some View {\n    NavigationView {\n        List {\n            ForEach(Array(cars.enumerated()), id: \\.offset) {\n\n                Text("\\($0.element) at \\($0.offset) index")\n            }\n        }\n    }\n}\n
Run Code Online (Sandbox Code Playgroud)\n

结果:

\n
//   Aurus at 0 index\n//   Bentley at 1 index\n//   Cadillac at 2 index\n//   Genesis at 3 index\n
Run Code Online (Sandbox Code Playgroud)\n


\n

聚苯乙烯

\n

最初,我发布了一个带有所有 Swift 开发人员都习惯的“通用”表达式的答案,但是,感谢 @loremipsum,我更改了它。正如 WWDC 2021 Demystify SwiftUI视频(时间 33:40)中所述,数组索引从\\.self身份(关键路径)来看并不稳定。

\n
ForEach(0 ..< cars.count, id: \\.self) {     // \xe2\x80\x93 NOT STABLE\n    Text("\\(cars[$0]) at \\($0) index")\n}\n
Run Code Online (Sandbox Code Playgroud)\n

  • 使用计数和索引。该答案的所有索引版本都被认为是不安全的。所列举的版本是最可接受的版本。 (2认同)