首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >问答首页 >如何在.enumerated()中与ForEach一起使用SwiftUI?

如何在.enumerated()中与ForEach一起使用SwiftUI?
EN

Stack Overflow用户
提问于 2019-12-11 22:39:16
回答 4查看 27.8K关注 0票数 54

下面是一个简单的SwiftUI列表,如预期的那样工作:

代码语言:javascript
运行
复制
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()来枚举项时,我会得到以下错误:

在“ForEach”上引用初始化器'init(_:id:content:)‘要求’(偏移: Int,元素: Int)‘符合'Hashable’。

在“ForEach”上引用初始化器'init(_:id:content:)‘需要“EnumeratedSequence”符合“RandomAccessCollection”

我该怎么做呢?

EN

回答 4

Stack Overflow用户

回答已采纳

发布于 2020-07-29 02:55:32

TL;DR

警告:如果您养成了在ForEach中使用enumerated()的习惯,那么有一天您可能会遇到EXC_BAD_INSTRUCTIONFatal error: Index out of bounds异常。这是因为并非所有集合都有基于0的索引。

更好的默认方法是使用zip

代码语言:javascript
运行
复制
ForEach(Array(zip(items.indices, items)), id: \.0) { index, item in
  // index and item are both safe to use here
}

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-based方法。

Apple枚举函数的文档也提到了这一点:

代码语言:javascript
运行
复制
    /// 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()可能会导致不明显的错误。

以这个片段为例:

代码语言:javascript
运行
复制
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,而不会导致异常。但是,如果我们使用的不是item,而是items[offset],那么很容易引发异常。

最后,行print(items[index])很容易导致异常,因为索引(实际上是偏移量)可能超出界限。

因此,更安全的方法是始终使用本文顶部提到的zip方法。

选择zip的另一个原因是,如果尝试在不同的集合(例如Set)中使用相同的代码,则在索引到类型(items[index])时可能会出现以下语法错误:

无法将“Int”类型的值转换为预期的参数类型“Set.Index”

通过使用基于zip的方法,仍然可以对集合进行索引。

如果计划经常使用它,也可以使用create an extension on collection

您可以在游乐场中测试所有这些:

代码语言:javascript
运行
复制
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)

更新:

  • 删除了提到您可以使用\.1的部分,因为Peacemoon指出这可能会导致问题。另外,我很确定如果您的项目符合Identifiable,那么首先使用zip是没有意义的,您应该能够只做Identifiable
票数 97
EN

Stack Overflow用户

发布于 2019-12-11 22:39:16

枚举此集合时,枚举中的每个元素都是类型的元组:

代码语言:javascript
运行
复制
 (offset: Int, element: Int)

因此,id param应该从id: \.self更改为id: \.element

代码语言:javascript
运行
复制
ForEach(items.enumerated(), id: \.element) { ...

但是,在进行此更改后,仍然会得到错误:

在“ForEach”上引用初始化器'init(_:id:content:)‘要求“EnumeratedSequence”符合“RandomAccessCollection”

因为ForEach需要对数据的随机访问,但是枚举只允许按顺序访问.若要解决此问题,请将枚举转换为数组。

代码语言:javascript
运行
复制
ForEach(Array(items.enumerated()), id: \.element) { ...

这里有一个扩展,您可以使用它来使这更容易一些:

代码语言:javascript
运行
复制
extension Collection {
  func enumeratedArray() -> Array<(offset: Int, element: Self.Element)> {
    return Array(self.enumerated())
  }
}

还有一个可以在(macos) Xcode操场上运行的示例:

代码语言:javascript
运行
复制
import AppKit
import PlaygroundSupport
import SwiftUI

extension Collection {
  func enumeratedArray() -> Array<(offset: Int, element: Self.Element)> {
    return Array(self.enumerated())
  }
}

struct App: View {
  let items = 100...200
  var body: some View {
    List {
      ForEach(items.enumeratedArray(), id: \.element) { index, item in
        Text("\(index): Item \(item)")
      }
    }.frame(width: 200, height: 200)
  }
}

PlaygroundPage.current.liveView = NSHostingView(rootView: App())
票数 38
EN

Stack Overflow用户

发布于 2019-12-12 18:47:52

在大多数情况下,您不需要enumerate,因为它有点慢。

代码语言:javascript
运行
复制
struct App: View {
    let items = Array(100...200)
    var body: some View {
        List {
           ForEach(items.indices, id: \.self) { index in
               Text("Item \(self.items[index])")
           }
        }.id(items).frame(width: 200, height: 200)
    }
}
票数 14
EN
页面原文内容由Stack Overflow提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://stackoverflow.com/questions/59295206

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档