【问题标题】:SwiftUI iterating through dictionary with ForEachSwiftUI 使用 ForEach 遍历字典
【发布时间】:2019-11-02 15:47:01
【问题描述】:

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

通用结构 'ForEach' 要求 '[String : Int]' 符合 'RandomAccessCollection'

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

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

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

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

【问题讨论】:

  • 在 WWDC21 上,Apple 宣布了 Collections 软件包,其中包括 OrderedDictionary,可与 ForEach 无缝协作 - 请参阅 this answer

标签: swift dictionary swiftui


【解决方案1】:

您可以对字典进行排序以获取 (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)
                }
            }
        }
    }
}

【讨论】:

  • 比其他答案好多了
  • ForEach(Array(dict.keys), id: \.self) 也可以,不需要排序。
  • 如果我使用 viewModel 会怎样?
  • 请理解,如果您使用这种方法,您的数据必须保持静态。没有绑定等。
  • 我得到:“通用结构 'ForEach' 要求 [String : [String]] 符合 'RandomAccessCollection'”如果我不排序,并且“类型 '[String: [String]]如果我尝试,则无法符合“可比较”
【解决方案2】:

由于它是无序的,唯一的办法就是将它放入一个数组中,这很简单。但是数组的顺序会有所不同。

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

【讨论】:

    【解决方案3】:

    简单的回答:没有。

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

    ForEach 自动监视他的集合中的变化

    我建议你看谈话:)

    您不能使用 ForEach,因为:

    1. 它监视收藏并监控动作。使用未分类的字典是不可能的。
    2. 当重用视图时(比如 UITableView 在单元格可以回收时重用单元格,ListUITableViewCells 支持,我认为 ForEach 正在做同样的事情),它需要计算什么要显示的单元格。它通过从数据源查询索引路径来做到这一点。从逻辑上讲,如果数据源是无序的,索引路径是没有用的。

    【讨论】:

    • 啊,好吧,所以它不起作用的原因是因为 ForEach 按照集合的顺序监视运动。我想如果顺序不重要,那么让它工作的唯一方法是将键复制到一个数组中然后迭代它?
    • @RPatel99 这是我知道的为什么你不应该/不能使用字典的两个原因之一。我更新了答案。在使用带有可重用单元格的 UITableView/UICollectionView 时,这实际上与您不能使用字典的原因相同
    • 好吧,这是有道理的。而且我在 SwiftUI 上观看了大部分 WWDC 视频,但信息量很大,所以很容易错过一些东西 :)
    【解决方案4】:

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

    【讨论】:

    • 如果我使用具有 dict 的 viewModel 会怎样
    • 仅适用于包含“可比较”元素的字典
    【解决方案5】:

    有序字典

    在 WWDC21 上,Apple 发布了 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("\($0)")
                }
            }
        }
    }
    

    注意:目前Collections 可作为单独的package 使用,因此您需要将import 发送到您的项目中。


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

    【讨论】:

    • 那太好了,但是如果我有一个普通的字典,我怎么能把它转换成orderedDict 只是铸造?
    • @Piakkaa 你可以做let orderedDict = OrderedDictionary(uniqueKeys: dict.keys, values: dict.values)。请注意,因为dict 是无序的,您可能需要对orderedDict 进行排序以强制执行一致的顺序。
    • id: \.self 是一个 hack,但 SwiftUI 确实需要一个符合 Identifiable 的自定义 Item 结构才能正常工作。
    • @malhal 不,那不是真的。字典中的键保证是唯一的,这意味着我们可以轻松地将它们用作id。不使用id: \.self 的想法是在我们不能保证对象是唯一的时避免冲突。这不是这里的情况。
    • 没有比这更多的列表。当数据为只读时,您的 hack 可能会起作用,但如果有人知道这一点并继续尝试编辑它,那么当您提供的字符串键从字典中删除时,List 将崩溃。您必须给 List 数据并告诉它该数据的哪个属性是 id,它们不能是独立的。
    【解决方案6】:

    我也试图用枚举/整数对字典来解决这个问题。正如 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()
        }
    }
    

    【讨论】:

      【解决方案7】:

      我是这样实现的:

      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 的 Optional。在生产中,您将对此 Optional 执行检查,以便以更安全的方式展开它,但这是一个概念证明。

      【讨论】:

        【解决方案8】:

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

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

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

        我强调了@J.Doe 和 cmets 的答案,即无序/随机集合 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
            }
        }
        

        【讨论】:

          【解决方案9】:

          不,您不能将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)
              }
          }
          

          【讨论】:

          • 这并不完全正确。除了使用 var id: String { name } 使 NamedFont Identifiable 之外,您还可以使用 ForEach(namedFonts, id: \.name)。 “已识别数据的集合”不需要符合文档中所述的 Identifiable:集合的元素必须符合 Identifiable 或者您需要向 ForEach 提供一个 id 参数初始化器。 你的方式只是硬币的一面。另外,id: .\self 不是黑客,虽然我承认它被错误地使用了很多次。
          猜你喜欢
          • 2022-12-18
          • 2020-05-19
          • 2022-01-11
          • 1970-01-01
          • 1970-01-01
          • 2010-12-31
          • 1970-01-01
          • 2016-05-21
          • 2019-09-24
          相关资源
          最近更新 更多