SwiftUI 使用 ForEach 遍历字典



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

泛型结构 'ForEach' 要求 '[String : Int]' 符合 'RandomAccessCollection'

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

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

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

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

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

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

或遍历字典的键

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

var body: some View {
List {
ForEach(Array(dict.keys), id: .self) { key in
Section(header: Text(key)) {
Text(dict[key] ?? "")
}
}
}
}
}

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

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

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作为单独的包提供,因此您需要将其导入到项目中。


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

  • WWDC 会议 - 了解 Swift 算法和集合包
  • swift.org 公告 - 推出 Swift Collections

简单的答案:不。

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

ForEach 会自动监视其收藏中的更改

我建议你看演讲:)

您不能使用 ForEach,因为:

  1. 它监视集合并监视移动。用一本无用的字典是不可能的。
  2. 当重用视图时(例如UITableView在可以回收单元格时重用单元格,ListUITableViewCells支持,我认为ForEach也在做同样的事情),它需要计算要显示的单元格。它通过从数据源查询索引路径来实现此目的。从逻辑上讲,如果数据源是无序的,则索引路径是无用的。

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

如果您使用的是 swift-collection 的 OrderedDictionary(OrderedCollections 的一部分),您可以执行以下操作:

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

确保键对象是可识别的,以便此操作正常工作,否则调整 ForEach 上的 id 参数。

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

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

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

我通过@J.Doe强调了答案,并评论说无序/随机集合Dictionary可能不是与表视图单元格一起使用的最佳解决方案(AppKitNSTableView/UIKitUITableView/SwiftUIList行)。

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

但是,如果您必须使用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
}
}

我认为最干净的方法(如果您不想为此设置包依赖项)就是只使用元组:

let myTuple: [(key: String, value: Int)] = [("test1": 1), ("test2": 2), ("test3": 3)]
...
ForEach(myTuple, id:.0) {...}

我也试图用枚举/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()
}
}

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

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

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

不,您不能将ForEachViewDictionary一起使用。您可以尝试,但它可能会崩溃,特别是如果您使用id: .self,枚举或指示黑客。如文档中所示,要正确使用ForEachView,您需要一个"已识别数据的集合",您可以通过创建一个符合 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)
}
}

最新更新