使用 ForEach 迭代 dictionary 的 SwiftUI

有没有办法在 ForEach循环中迭代 Dictionary

泛型结构‘ ForEach’要求‘[ String: Int ]’符合‘ RRandom AccessCollection’

那么有没有办法使迅捷字典符合 RandomAccessCollection,或者是不可能的,因为字典是无序的?

我尝试过的一件事就是迭代字典的键:

let dict: [String: Int] = ["test1": 1, "test2": 2, "test3": 3]
...
ForEach(dict.keys) {...}

但是 keys不是 String的数组,它的类型是 Dictionary<String, Int>.Keys(不确定是什么时候改变的)。我知道我可以编写一个 helper 函数,它接收字典并返回一个键数组,然后我可以迭代这个数组,但是有没有一种内置的方法来做到这一点,或者一种更优雅的方法呢?我可以扩展 Dictionary,使它符合 RandomAccessCollection或其他什么?

68756 次浏览

答案很简单,不。

正如你正确指出的,字典是无序的。ForEach 监视其集合中的更改。这些更改包括插入、删除、 行动和更新。如果发生任何这些更改,将触发更新。参考文献: https://developer.apple.com/videos/play/wwdc2019/204/ at 46:10:

ForEach 会自动监视其集合中的更改

我建议你看看这个演讲:)

您不能使用 ForEach,因为:

  1. 它可以监视收藏品和动作,用一本没有编号的字典是不可能的。
  2. 当重用视图时(就像 UITableView在单元格可以回收时重用单元格,ListUITableViewCells支持,我认为 ForEach也在做同样的事情) ,它需要计算显示哪个单元格。它通过从数据源查询索引路径来实现这一点。从逻辑上讲,如果数据源是无序的,则索引路径是无用的。

因为它是无序的,所以唯一的方法是将它放入一个数组中,这非常简单。但是数组的顺序会有所不同。

struct Test : View {
let dict: [String: Int] = ["test1": 1, "test2": 2, "test3": 3]
var body: some View {
let keys = dict.map{$0.key}
let values = dict.map {$0.value}


return List {
ForEach(keys.indices) {index in
HStack {
Text(keys[index])
Text("\(values[index])")
}
}
}
}
}


#if DEBUG
struct Test_Previews : PreviewProvider {
static var previews: some View {
Test()
}
}
#endif

您可以对字典进行排序,以获取(键,值)元组数组,然后使用它。

struct ContentView: View {
let dict = ["key1": "value1", "key2": "value2"]
    

var body: some View {
List {
ForEach(dict.sorted(by: >), id: \.key) { key, value in
Section(header: Text(key)) {
Text(value)
}
}
}
}
}

我也试图用 enum/Int 对的 Dictionary 来解决这个问题。我有效地将其转换为数组,安德烈建议,但不使用地图,我只是强制键。

enum Fruits : Int, CaseIterable {
case Apple = 0
case Banana = 1
case Strawberry = 2
case Blueberry = 3
}


struct ForEachTest: View {
var FruitBasket : [Fruits: Int] = [Fruits.Apple: 5, Fruits.Banana : 8, Fruits.Blueberry : 20]
var body: some View {
VStack {
ForEach([Fruits](FruitBasket.keys), id:\Fruits.hashValue) { f in
Text(String(describing: f) + ": \(self.FruitBasket[f]!)")
}
}
}
}


struct ForEachTest_Previews: PreviewProvider {
static var previews: some View {
ForEachTest()
}
}

Xcode: 11.4.1 ~

    ...
var testDict: [String: Double] = ["USD:": 10.0, "EUR:": 10.0, "ILS:": 10.0]
...
ForEach(testDict.keys.sorted(), id: \.self) { key in
HStack {
Text(key)
Text("\(testDict[key] ?? 1.0)")
}
}
...

更多细节:

final class ViewModel: ObservableObject {
@Published
var abstractCurrencyCount: Double = 10
@Published
var abstractCurrencytIndex: [String: Double] = ["USD:": 10.0, "EUR:": 15.0, "ILS:": 5.0]
}


struct SomeView: View {
@ObservedObject var vm = ViewModel()
var body: some View {
NavigationView {
List {
ForEach(vm.abstractCurrencytIndex.keys.sorted(), id: \.self) { key in
HStack {
Text(String(format: "%.2f", self.vm.abstractCurrencyCount))
Text("Abstract currency in \(key)")
Spacer()
Text(NumberFormatter.localizedString(from: NSNumber(value: self.vm.abstractCurrencytIndex[key] ?? 0.0), number: .currency))
}
}
}
}
}
}

以下是我如何实现这一点:

struct CartView: View {
var cart:[String: Int]


var body: some View {
List {
ForEach(cart.keys.sorted()) { key in
Text(key)
Text("\(cart[key]!)")
}
}
}
}

第一个文本视图将输出一个字符串键。 第二个文本视图将输出在该键是一个 Int。 啊!接下来是展开包含这个 Int 的可选项。在生产环境中,您将对这个可选项执行检查,以便以更安全的方式展开它,但是这是一个概念验证。

我在使用本文的一些答案中的代码时遇到的语法错误帮助我为自己的问题找到了一个解决方案..。

使用包含以下内容的字典:

  • key = a CoreDataEntity;
  • value = 关系中该实体的实例数(类型为 NSSet)。

我用@J 标出了答案。Doe 和注释,无序/随机集合 Dictionary可能不是用于表视图单元格的最佳解决方案(AppKit NSTableView/UIKit UITableView/SwiftUI List行)。

随后,我将重新构建代码以使用数组。

但是,如果你必须使用 Dictionary,这里是我的工作解决方案:

struct MyView: View {
    

var body: some View {
        

// ...code to prepare data for dictionary...


var dictionary: [CoreDataEntity : Int] = [:]


// ...code to create dictionary...


let dictionarySorted = dictionary.sorted(by: { $0.key.name < $1.key.name })
// where .name is the attribute of the CoreDataEntity to sort by
        

VStack {  // or other suitable view builder/s
ForEach(dictionarySorted, id: \.key) { (key, value) in
Text("\(value) \(key.nameSafelyUnwrapped)")
}
}
}
}


extension CoreDataEntity {
var nameSafelyUnwrapped: String {
guard let name = self.name else {
return String() // or another default of your choosing
}
return name
}
}

OrderedDictionary

在 WWDC21上,苹果公司发布了包括 OrderedDictionary在内的 Collections软件包。

现在,我们只需要更换:

let dict: [String: Int] = ["test1": 1, "test2": 2, "test3": 3]

与:

let dict: OrderedDictionary = ["test1": 1, "test2": 2, "test3": 3]

或者,我们可以从另一个中引入一个:

let dict: [String: Int] = ["test1": 1, "test2": 2, "test3": 3]
let orderedDict = OrderedDictionary(uniqueKeys: dict.keys, values: dict.values)

请注意,因为 dict是无序的,所以您可能需要对 orderedDict进行排序,以强制执行一致的顺序。


下面是一个如何在 SwiftUI 视图中使用它的例子:

import Collections
import SwiftUI


struct ContentView: View {
let dict: OrderedDictionary = ["test1": 1, "test2": 2, "test3": 3]


var body: some View {
VStack {
ForEach(dict.keys, id: \.self) {
Text("\($0)")
}
}
}
}

注意: 目前 Collections是作为一个单独的 包裹提供的,所以您需要 进口它到您的项目。


你可在此找到更多资料:

不行,ForEach View不能和 Dictionary一起使用。您可以尝试,但它可能会崩溃,特别是如果您使用的 id: .\self,枚举或索引黑客。如图 在文件中所示,为了正确使用 ForEach View,你需要一个“标识数据集合”,你可以通过创建一个自定义结构来创建这个结构,这个结构符合 Identity,并且使用一个包含如下结构的数组:

private struct NamedFont: Identifiable {
let name: String
let font: Font
var id: String { name } // or let id = UUID()
}


private let namedFonts: [NamedFont] = [
NamedFont(name: "Large Title", font: .largeTitle),
NamedFont(name: "Title", font: .title),
NamedFont(name: "Headline", font: .headline),
NamedFont(name: "Body", font: .body),
NamedFont(name: "Caption", font: .caption)
]


var body: some View {
ForEach(namedFonts) { namedFont in
Text(namedFont.name)
.font(namedFont.font)
}
}

如果您正在使用 快速收集的 OrderedDictionary (OrderedCollection 的一部分) ,您可以执行以下操作:

var states: OrderedDictionary<Player, String>
ForEach(states.elements, id:\.key) { player, state in
PlayerRow(player: player, state: state)
}

确保键对象是可识别的,否则将调整 ForEach 上的 id 参数。