问题描述
这是一个按预期工作的简单 SwiftUI 列表:
Here's a simple SwiftUI List that works as expected:
struct App: View {
let items = Array(100...200)
var body: some View {
List {
ForEach(items, id: \.self) { index, item in
Text("Item \(item)")
}
}.frame(width: 200, height: 200)
}
}
但是当我尝试通过将 items
替换为 items.enumerated()
来枚举项目时,我收到以下错误:
but when I try to enumerate items by replacing items
with items.enumerated()
I get these errors:
在 'ForEach' 上引用初始值设定项 'init(_:id:content:)' 要求 '(offset: Int, element: Int)' 符合 'Hashable'
在 'ForEach' 上引用初始值设定项 'init(_:id:content:)' 需要 'EnumeratedSequence'符合 'RandomAccessCollection'
Referencing initializer 'init(_:id:content:)' on 'ForEach' requires that 'EnumeratedSequence<[Int]>' conform to 'RandomAccessCollection'
我该如何进行这项工作?
How do I make this work?
推荐答案
TL;DR
警告:如果您养成将 enumerated()
与 ForEach
一起使用的习惯,那么有一天您可能会使用 EXC_BAD_INSTRUCTION
或 Fatal error: Index out of bounds
异常.这是因为并非所有集合都具有从 0 开始的索引.
Warning: If you get in the habit of using enumerated()
with ForEach
, you may one day end up with EXC_BAD_INSTRUCTION
or Fatal error: Index out of bounds
exceptions. This is because not all collections have 0-based indexes.
更好的默认设置是使用 zip
代替:
A better default is to use zip
instead:
ForEach(Array(zip(items.indices, items)), id: \.0) { index, item in
// index and item are both safe to use here
}
(如果您的商品符合Identifiable
,您也可以使用id: \.1
.)
(You can also use id: \.1
if your items conform to Identifiable
.)
Point-Free 提到在生产中依赖 enumerated()
和 ForEach
是不安全的,因为并非所有集合都是基于零索引的:
The folks over at Point-Free mentioned that it's not safe to rely on enumerated()
with ForEach
in production since not all collections are zero-index based:
从技术上讲,这不是执行此操作的最正确方法.用它的索引集合压缩 todos
数组会更正确,也更详细.在这种情况下,我们是安全的,因为我们正在处理一个简单的基于 0 的索引数组,但如果我们在生产中这样做,我们可能应该基于 zip
的方法.
Apple 的枚举函数文档也提到了这一点:
Apple's documentation for the enumerated function mentions this as well:
/// Returns a sequence of pairs (*n*, *x*), where *n* represents a
/// consecutive integer starting at zero and *x* represents an element of
/// the sequence.
///
/// This example enumerates the characters of the string "Swift" and prints
/// each character along with its place in the string.
///
/// for (n, c) in "Swift".enumerated() {
/// print("\(n): '\(c)'")
/// }
/// // Prints "0: 'S'"
/// // Prints "1: 'w'"
/// // Prints "2: 'i'"
/// // Prints "3: 'f'"
/// // Prints "4: 't'"
///
/// When you enumerate a collection, the integer part of each pair is a counter
/// for the enumeration, but is not necessarily the index of the paired value.
/// These counters can be used as indices only in instances of zero-based,
/// integer-indexed collections, such as `Array` and `ContiguousArray`. For
/// other collections the counters may be out of range or of the wrong type
/// to use as an index. To iterate over the elements of a collection with its
/// indices, use the `zip(_:_:)` function.
///
/// This example iterates over the indices and elements of a set, building a
/// list consisting of indices of names with five or fewer letters.
///
/// let names: Set = ["Sofia", "Camilla", "Martina", "Mateo", "Nicolás"]
/// var shorterIndices: [Set<String>.Index] = []
/// for (i, name) in zip(names.indices, names) {
/// if name.count <= 5 {
/// shorterIndices.append(i)
/// }
/// }
///
/// Now that the `shorterIndices` array holds the indices of the shorter
/// names in the `names` set, you can use those indices to access elements in
/// the set.
///
/// for i in shorterIndices {
/// print(names[i])
/// }
/// // Prints "Sofia"
/// // Prints "Mateo"
///
/// - Returns: A sequence of pairs enumerating the sequence.
///
/// - Complexity: O(1)
在您的特定情况下 enumerated()
很好用,因为您使用的是基于 0 的索引数组,但是由于上述细节,依赖于 enumerated()
总是会导致不明显的错误.
In your specific case enumerated()
is fine to use since you are using a 0-based index array, however due to the details above, relying on enumerated()
all the time can lead to non-obvious errors.
以这个片段为例:
ForEach(Array(items.enumerated()), id: \.offset) { offset, item in
Button(item, action: { store.didTapItem(at: offset) })
}
// ...
class Store {
var items: ArraySlice<String>
func didTapItem(at index: Int) {
print(items[index])
}
}
首先注意到我们用 Button(item...
躲过了一个子弹,因为 enumerated()
保证了 item
可以被直接访问不会导致异常.但是,如果我们使用 items[offset]
代替 item
,则很容易引发异常.
First notice that we dodged a bullet with Button(item...
since enumerated()
has guaranteed that item
can be accessed directly without causing an exception. However, if instead of item
we used items[offset]
, an exception could easily be raised.
最后,行 print(items[index])
很容易导致异常,因为索引(实际上是偏移量)可能超出范围.
Finally, the line print(items[index])
can easily lead to an exception since the index (really the offset) can be out of bounds.
因此,更安全的方法是始终使用本文顶部提到的 zip
方法.
Therefore, a safer approach is to always use the zip
method mentioned at the top of this post.
喜欢 zip
的另一个原因是,如果您尝试将相同的代码与不同的集合(例如 Set)一起使用,则在对类型 (items[index]
):
Another reason to prefer zip
is that if you tried using the same code with a different Collection (e.g. Set) you could get the following syntax error when indexing into the type (items[index]
):
无法将Int"类型的值转换为预期的参数类型Set.Index"
通过使用基于 zip
的方法,您仍然可以索引到集合中.
By using the zip
based approach, you can still index into the collection.
如果您打算经常使用它,您还可以创建集合扩展.
You could also create an extension on collection if you plan on using it often.
您可以在 Playground 中测试这一切:
You can test this all out in a Playground:
import PlaygroundSupport
import SwiftUI
// MARK: - Array
let array = ["a", "b", "c"]
Array(array.enumerated()) // [(offset 0, element "a"), (offset 1, element "b"), (offset 2, element "c")]
Array(zip(array.indices, array)) // [(.0 0, .1 "a"), (.0 1, .1 "b"), (.0 2, .1 "c")]
let arrayView = Group {
ForEach(Array(array.enumerated()), id: \.offset) { offset, element in
PrintView("offset: \(offset), element: \(element)")
Text("value: \(array[offset])")
}
// offset: 0, element: a
// offset: 1, element: b
// offset: 2, element: c
ForEach(Array(zip(array.indices, array)), id: \.0) { index, element in
PrintView("index: \(index), element: \(element)")
Text("value: \(array[index])")
}
// index: 0, element: a
// index: 1, element: b
// index: 2, element: c
}
// MARK: - Array Slice
let arraySlice = array[1...2] // ["b", "c"]
Array(arraySlice.enumerated()) // [(offset 0, element "b"), (offset 1, element "c")]
Array(zip(arraySlice.indices, arraySlice)) // [(.0 1, .1 "b"), (.0 2, .1 "c")]
// arraySlice[0] // ❌ EXC_BAD_INSTRUCTION
arraySlice[1] // "b"
arraySlice[2] // "c"
let arraySliceView = Group {
ForEach(Array(arraySlice.enumerated()), id: \.offset) { offset, element in
PrintView("offset: \(offset), element: \(element)")
// Text("value: \(arraySlice[offset])") ❌ Fatal error: Index out of bounds
}
// offset: 0, element: b
// offset: 1, element: c
ForEach(Array(zip(arraySlice.indices, arraySlice)), id: \.0) { index, element in
PrintView("index: \(index), element: \(element)")
Text("value: \(arraySlice[index])")
}
// index: 1, element: b
// index: 2, element: c
}
// MARK: - Set
let set: Set = ["a", "b", "c"]
Array(set.enumerated()) // [(offset 0, element "b"), (offset 1, element "c"), (offset 2, element "a")]
Array(zip(set.indices, set)) // [({…}, .1 "a"), ({…}, .1 "b"), ({…}, .1 "c")]
let setView = Group {
ForEach(Array(set.enumerated()), id: \.offset) { offset, element in
PrintView("offset: \(offset), element: \(element)")
// Text("value: \(set[offset])") // ❌ Syntax error: Cannot convert value of type 'Int' to expected argument type 'Set<String>.Index'
}
// offset: 0, element: a
// offset: 1, element: b
// offset: 2, element: c
ForEach(Array(zip(set.indices, set)), id: \.0) { index, element in
PrintView("index: \(index), element: \(element)")
Text("value: \(set[index])")
}
// index: Index(_variant: Swift.Set<Swift.String>.Index._Variant.native(Swift._HashTable.Index(bucket: Swift._HashTable.Bucket(offset: 0), age: -481854246))), element: a
// index: Index(_variant: Swift.Set<Swift.String>.Index._Variant.native(Swift._HashTable.Index(bucket: Swift._HashTable.Bucket(offset: 2), age: -481854246))), element: b
// index: Index(_variant: Swift.Set<Swift.String>.Index._Variant.native(Swift._HashTable.Index(bucket: Swift._HashTable.Bucket(offset: 3), age: -481854246))), element: c
}
// MARK: -
struct PrintView: View {
init(_ string: String) {
print(string)
self.string = string
}
var string: String
var body: some View {
Text(string)
}
}
let allViews = Group {
arrayView
arraySliceView
setView
}
PlaygroundPage.current.setLiveView(allViews)
这篇关于您如何在 SwiftUI 中将 .enumerated() 与 ForEach 一起使用?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持!