SwiftUI iterating through dictionary with ForEach

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

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

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

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.