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,因为:
- 它监视集合并监视运动。无法使用未排序的字典。
- 重用视图时(例如
UITableView
在单元格可以回收时重用单元格,List
由 UITableViewCells
支持,我认为 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 用于您的项目。
您可以在此处找到更多信息:
- WWDC session - Meet the Swift Algorithms and Collections packages
- swift.org公告-Introducing Swift Collections
不,您不能将 ForEach
View
与 Dictionary
一起使用。您可以尝试,但它可能会崩溃,特别是如果您使用 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)
}
}
有没有办法在 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,因为:
- 它监视集合并监视运动。无法使用未排序的字典。
- 重用视图时(例如
UITableView
在单元格可以回收时重用单元格,List
由UITableViewCells
支持,我认为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 用于您的项目。
您可以在此处找到更多信息:
- WWDC session - Meet the Swift Algorithms and Collections packages
- swift.org公告-Introducing Swift Collections
不,您不能将 ForEach
View
与 Dictionary
一起使用。您可以尝试,但它可能会崩溃,特别是如果您使用 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)
}
}