SwiftUI iterating through dictionary with ForEach

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 Strings, 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?


Solution 1:

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:

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:

  1. It watches a collection and monitors movements. Impossible with an unorered dictionary.
  2. When reusing views (like a UITableView reuses cells when cells can be recycled, a List is backed by UITableViewCells, and I think a ForEach 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 3:

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