在 SwiftUI 的 ForEach 中获取索引
Get index in ForEach in SwiftUI
我有一个数组,我想遍历它根据数组值初始化视图,并想根据数组项索引执行操作
当我遍历对象时
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
没有改变。
有解决办法吗?提前致谢。
这对我有用:
使用范围和计数
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
}
}
获取索引和元素。
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
以下方法的优点是,如果状态值发生变化,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({ [=10=] })
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({ [=11=] })
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
// ...
}
我基于令人敬畏的 Stone 解决方案为此目的创建了一个专用 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
...
}
这是一个简单的解决方案,但与上面的解决方案相比效率很低..
在你的点击操作中,通过你的项目
.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
得到一对index
和element
,element
作为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 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(items) { item in
CustomView(item: item)
.tapAction {
if let index = array.firstIndex(where: { [=11=].id == item.id }) {
self.doSomething(index)
}
}
}
您可以在他们的 Scrumdinger sample app tutorial.
中看到 Apple 这样做
guard let scrumIndex = scrums.firstIndex(where: { [=12=].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 }
我有一个数组,我想遍历它根据数组值初始化视图,并想根据数组项索引执行操作
当我遍历对象时
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
没有改变。
有解决办法吗?提前致谢。
这对我有用:
使用范围和计数
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
}
}
获取索引和元素。
Note that the API is mirrored to that of SwiftUI - this means that the initializer with the
id
parameter'scontent
closure is not a@ViewBuilder
.
The only change from that is theid
parameter is visible and can be changed
以下方法的优点是,如果状态值发生变化,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({ [=10=] })
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({ [=11=] })
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
// ...
}
我基于令人敬畏的 Stone 解决方案为此目的创建了一个专用 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
...
}
这是一个简单的解决方案,但与上面的解决方案相比效率很低..
在你的点击操作中,通过你的项目
.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
得到一对index
和element
,element
作为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 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(items) { item in
CustomView(item: item)
.tapAction {
if let index = array.firstIndex(where: { [=11=].id == item.id }) {
self.doSomething(index)
}
}
}
您可以在他们的 Scrumdinger sample app tutorial.
中看到 Apple 这样做guard let scrumIndex = scrums.firstIndex(where: { [=12=].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 }