SwiftUI 使用 ForEach 遍历字典

SwiftUI iterating through dictionary with ForEach

有没有办法在 ForEach 循环中遍历 Dictionary? Xcode 说

Generic struct 'ForEach' requires that '[String : Int]' conform to 'RandomAccessCollection'

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

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

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

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

简单回答:没有。

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

A ForEach automatically watches for changes in his collection

我推荐你看演讲:)

您不能使用 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{[=10=].key}
    let values = dict.map {[=10=].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

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

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 对的字典来解决这个问题。我按照 Andre 的建议有效地将它转换为数组,但我没有使用 map 我只是转换了键。

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]!)")
            }
        }
    }
}

第一个TextView会输出一个String类型的key。 第二个 Text View 将在该键处输出 Dict 的值,该键是一个 Int。 这 !接下来是解包包含此 Int 的 Optional。在生产中,您将对此 Optional 执行检查以获得更安全的解包方式,但这是概念证明。

我使用一些答案中的代码遇到的语法错误 post 帮助我找到了解决我自己问题的方法...

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

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

我强调@J.Doe 的回答,并评论说无序/随机集合 Dictionary 可能不是与 table 视图单元一起使用的最佳解决方案(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: { [=10=].key.name < .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
    }
}

有序词典

Apple 在 WWDC21 上宣布了 Collections 软件包,其中包括 OrderedDictionary(以及其他)。

现在,我们只需要替换:

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("\([=13=])")
            }
        }
    }
}

注意:目前 Collections 可作为单独的 package, so you need to import 用于您的项目。


您可以在此处找到更多信息:

不,您不能将 ForEach ViewDictionary 一起使用。您可以尝试,但它可能会崩溃,特别是如果您使用 id: .\self hack 或者如果您尝试从实际数据循环不同的数组。如图所示 in the documentation,要正确使用 ForEach View,您需要一个“已识别数据的集合”,您可以通过制作符合 Identifiable 的自定义结构并使用包含的数组来创建它结构如下:

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