SwiftUI iterating through dictionary with ForEach
SwiftDictionarySwiftuiSwift Problem Overview
Is there a way to iterate through a Dictionary
in a ForEach
loop? Xcode says
> Generic struct 'ForEach' requires that '[String : Int]' conform to 'RandomAccessCollection'
so is there a way to make Swift Dictionaries conform to RandomAccessCollection
, or is that not possible because Dictionaries are unordered?
One thing I've tried is iterating the dictionary's keys:
let dict: [String: Int] = ["test1": 1, "test2": 2, "test3": 3]
...
ForEach(dict.keys) {...}
But keys
is not an array of String
s, it's type is Dictionary<String, Int>.Keys
(not sure when that was changed). I know I could write a helper function that takes in a dictionary and returns an array of the keys, and then I could iterate that array, but is there not a built-in way to do it, or a way that's more elegant? Could I extend Dictionary
and make it conform to RandomAccessCollection
or something?
Swift Solutions
Solution 1 - Swift
You can sort your dictionary to get (key, value) tuple array and then use it.
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)
}
}
}
}
}
Solution 2 - Swift
Since it's unordered, the only way is to put it into an array, which is pretty simple. But the order of the array will vary.
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
Solution 3 - Swift
Simple answer: no.
As you correctly pointed out, a dictionary is unordered. The ForEach watches its collection for changes. These changes includes inserts, deletions, moves and update. If any of those changes occurs, an update will be triggered. Reference: https://developer.apple.com/videos/play/wwdc2019/204/ at 46:10:
> A ForEach automatically watches for changes in his collection
I recommend you watch the talk :)
You can not use a ForEach because:
- It watches a collection and monitors movements. Impossible with an unorered dictionary.
- When reusing views (like a
UITableView
reuses cells when cells can be recycled, aList
is backed byUITableViewCells
, and I think aForEach
is doing the same thing), it needs to compute what cell to show. It does that by querying an index path from the data source. Logically speaking, an index path is useless if the data source is unordered.
Solution 4 - Swift
OrderedDictionary
At WWDC21 Apple announced the Collections
package that includes OrderedDictionary
(among others).
Now, we just need to replace:
let dict: [String: Int] = ["test1": 1, "test2": 2, "test3": 3]
with:
let dict: OrderedDictionary = ["test1": 1, "test2": 2, "test3": 3]
Alternatively, we can init one from another:
let dict: [String: Int] = ["test1": 1, "test2": 2, "test3": 3]
let orderedDict = OrderedDictionary(uniqueKeys: dict.keys, values: dict.values)
Just note that because dict
is unordered, you may want to sort the orderedDict
to enforce the consistent order.
Here is an example how we can use it in a SwiftUI View:
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)")
}
}
}
}
Note: Currently Collections
is available as a separate package, so you need to import it to your project.
You can find more information here:
- WWDC session - Meet the Swift Algorithms and Collections packages
- swift.org announcement - Introducing Swift Collections
Solution 5 - Swift
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)")
}
}
...
more detail:
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))
}
}
}
}
}
}
Solution 6 - Swift
I was trying to figure this out as well with a Dictionary of enum/Int pairs. I am effectively converting it to array as Andre suggests, but instead of using map I just cast the keys.
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()
}
}
Solution 7 - Swift
Here is how I implemented this:
struct CartView: View {
var cart:[String: Int]
var body: some View {
List {
ForEach(cart.keys.sorted()) { key in
Text(key)
Text("\(cart[key]!)")
}
}
}
}
The first Text View will output the key which is a String. The second Text View will output the value of the Dict at that key which is an Int. The ! that follows this is to unwrap the Optional which contains this Int. In production you would perform checks on this Optional for a more safe way of unwrapping it, but this is a proof of concept.
Solution 8 - Swift
The syntax errors I encountered using code from some of the answers to this post helped me sort out a solution for my own problem...
Using a dictionary that contained:
key
= aCoreDataEntity
;value
= the count of instances of that entity from a relationship (typeNSSet
).
I highlight the answer by @J.Doe and comments that an unordered / random collection Dictionary
may not be the best solution to use with table view cells (AppKit NSTableView
/ UIKit UITableView
/ SwiftUI List
rows).
Subsequently, I'll be rebuilding my code to instead work with arrays.
But if you must work with a Dictionary
, here is my working solution:
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
}
}
Solution 9 - Swift
No, you can't use the ForEach
View
with Dictionary
. You can try but it will likely crash, especially if you use the id: .\self
hack of if you try to loop a different array from the actual data. As shown in the documentation, to use the ForEach
View
correctly, you need a "collection of identified data" which you can create by make a custom struct that conforms to Identifiable and use an array containing the structs as follows:
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)
}
}