在 SwiftUI 中获取 ForEach 中的索引

我有一个数组,我想通过它迭代初始化基于数组值的视图,并希望执行基于数组项目索引的操作

当我迭代对象时

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没有改变。

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

106923 次浏览

这对我有用:

使用范围和计数

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

我需要一个更通用的解决方案,它可以处理所有类型的数据(实现了 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闭包的初始化器是 没有 a @ViewBuilder
唯一的改变是 id参数是可见的,并且可以改变

下面这种方法的优点是,如果状态值发生变化,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()
}
}
}
}
}

另一种方法是:

枚举()

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

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

我为此创建了一个专用的 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(zip(data.indices, data)), id: \.0) { idx, item in
content(idx, item)
}
}
}

现在你可以这样使用它:

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

这里有一个简单的解决方案,尽管对于上述方案来说效率很低。

在您的点击动作,通过您的项目

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

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

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

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

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

我通常使用 enumerated得到一对 indexelementelement作为 id

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/

ForEach是 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中使用索引、枚举或新的 Array。必须为 ForEach提供可识别项的实际数组。这样 SwiftUI 就可以对周围的行进行动画来匹配数据,显然这不适用于索引,例如,如果0处的行移动到1处,它的索引仍然是0。

为了解决索引的问题,你只需要像这样查找索引:

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

你可以看到苹果在他们的 Scrumdinger 示例应用程序教程中这样做。

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

就像他们提到的,你可以使用 array.indices来实现这个目的 但是 请记住,您得到的索引是从数组的最后一个元素开始的,要解决这个问题,您必须使用这个: array.indices.reversed()还应该为 ForEach 提供一个 id。 这里有一个例子:

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

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

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

结果:

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


另外。

最初,我发布了一个所有 Swift 开发人员都习惯使用的“常见”表达式,但是,由于@loremipsum,我改变了它。如 WWDC 2021 揭开 SwiftUI 的神秘面纱视频(时间33:40)所述,从 \.self身份(关键路径)数组索引不稳定。

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