95

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?

RPatel99
  • 7,448
  • 2
  • 37
  • 45
  • 3
    At WWDC21 Apple announced the `Collections` package that includes `OrderedDictionary` which works seamlessly with `ForEach` - see [this answer](https://stackoverflow.com/a/68023633/8697793). – pawello2222 Jun 17 '21 at 17:10

11 Answers11

156

You can sort your dictionary to obtain a tuple array of (key, value) pairs and then utilize 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)
                }
            }
        }
    }
}

or loop over the keys of the dictionary

struct ContentView: View {
    let dict = ["key1": "value1", "key2": "value2"]
    
    var body: some View {
        List {
            ForEach(Array(dict.keys), id: \.self) { key in
                Section(header: Text(key)) {
                    Text(dict[key] ?? "")
                }
            }
        }
    }
}
ozmpai
  • 2,454
  • 2
  • 14
  • 19
  • 1
    much better than other answers – Arafin Russell Jun 04 '20 at 09:06
  • 48
    `ForEach(Array(dict.keys), id: \.self)` works too, no need for sorting. – Abhijit Sarkar Jul 13 '20 at 10:54
  • what if im using a viewModel? – Di Nerd Apps Jul 20 '20 at 14:33
  • 8
    Just understand that if you use this approach, your data must remain static. No Bindings, etc. – cdeerinck Aug 03 '20 at 19:33
  • 1
    What does `id: \.self` or `id: \.key` do? Are they the same? – Jason May 07 '21 at 00:32
  • 10
    I get : "Generic struct 'ForEach' requires that [String : [String]] conform to 'RandomAccessCollection'" if I don't sort, and "Type '[String: [String]] cannot conform to 'Comparable'" if I try to – ice-wind Oct 20 '21 at 16:26
  • 1
    @Jason ```id:``` is used in ```ForEach``` to identify every item in your array, i.e., every item should be unique when they're presenting. You can specify anything you want to be the ID of each item. Note that you my need to make your model struct conform to ```Identifiable``` in some circumstances. – RoyRao Dec 06 '21 at 05:54
  • The comment above by @AbhijitSarkar is the best answer if you don't want to resort your dictionary. – dbDev Aug 29 '22 at 20:58
26

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
Andre Carrera
  • 2,606
  • 2
  • 12
  • 15
22

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:

pawello2222
  • 46,897
  • 22
  • 145
  • 209
  • thats wonderful, but if I have a normal dict, how can I convert it to orderedDict just casting? – Piakkaa Jun 22 '21 at 05:47
  • 1
    @Piakkaa You can do `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. – pawello2222 Jun 22 '21 at 09:23
  • `id: \.self` is a hack though, SwiftUI really needs a custom Item struct that conforms to Identifiable to work properly. – malhal Jul 01 '21 at 18:49
  • 1
    @malhal No, that's not true. Keys in dictionary are guaranteed to be unique which means we can easily use them as `id`. The idea of not using `id: \.self` is to avoid clashes when we can't guarantee that objects are unique. This is not the case here. – pawello2222 Jul 01 '21 at 19:21
  • No there is more to List than that. Your hack might work when the data is readonly but if someone learns that and goes on to try to edit it, then List will crash when a string key you have given it is removed from the dict. You have to give List the data and tell it what property of that data is the id, they cannot be independent. – malhal Jul 02 '21 at 16:11
  • From the docs, ForEach requires a "underlying collection of identified data" https://developer.apple.com/documentation/swiftui/foreach?changes=latest_minor – malhal Jul 02 '21 at 23:23
  • @malhal Please read the docs more carefully. What you said is only partially true. The "collection of identified data" **doesn't need** to conform to Identifiable as stated in the docs: *Either the collection’s elements must conform to Identifiable or you need to provide an id parameter to the ForEach initializer.* Also, `id: .\self` is not a hack - in this case it's an equivalent of creating a struct with a single `key` property and conforming it to Identifiable with `var id: String { key }`. – pawello2222 Jul 03 '21 at 22:44
  • I fully understand the documentation and am trying to help you understand is too. The dict is the data and you haven’t given the data to ForEach. You created an array of keys and gave that to ForEach. That is the main problem here because you cannot use the dict inside the ForEach if you haven’t told it how to identify each element using the definition of “collection of identified data”. Your array of strings is external to the collection of data which is a big problem and will cause crashes in certain situations if you attempt to access the dict. – malhal Jul 04 '21 at 08:03
  • It’s possible you could make it not crash if the keys are an @State (and the dict) and use .onChange(dict) { keys = dict.keys } but I haven’t tried it and it still wouldn’t be fully correct. – malhal Jul 04 '21 at 08:11
  • 1
    Just wanted to say thanks for sharing this. I had no idea that Apple released this. – Robert Jun 03 '22 at 16:25
20

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.
Antony Stubbs
  • 13,161
  • 5
  • 35
  • 39
J. Doe
  • 12,159
  • 9
  • 60
  • 114
  • Ah ok so it the reason it doesn't work is because ForEach watches for movements in the order of the collection. I guess that if order isn't important, the only way to make it work would be to copy the keys into an array and then iterate that? – RPatel99 Jun 19 '19 at 20:57
  • 1
    @RPatel99 It is one of the two reasons known to me why you shouldn't/can't use a dictionary. I updated by answer. It is really nothing more than the same reasons you can not use a dictionary when using a UITableView/UICollectionView with reuseable cells – J. Doe Jun 19 '19 at 21:11
  • Ok that makes sense. And I have watched a majority of the WWDC videos on SwiftUI but there was a lot of information so its easy to miss some things :) – RPatel99 Jun 20 '19 at 00:08
8

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))
                    }
                }
            }
        }
    }
}
Dim Novo
  • 367
  • 3
  • 8
4

If you're using swift-collection's OrderedDictionary (part of OrderedCollections), you can do the following:

var states: OrderedDictionary<Player, String>
ForEach(states.elements, id:\.key) { player, state in
    PlayerRow(player: player, state: state)
}

Make sure the key object is identifiable for this to work, otherwise adjust the id parameter on the ForEach.

abegehr
  • 438
  • 5
  • 17
1

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 = a CoreDataEntity;
  • value = the count of instances of that entity from a relationship (type NSSet).

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
    }
}
andrewbuilder
  • 3,629
  • 2
  • 24
  • 46
1

I think the cleanest way (if you do not want to setup a package dependency just for this) is to just use Tuples instead:

let myTuple: [(key: String, value: Int)] = [("test1": 1), ("test2": 2), ("test3": 3)]
...
ForEach(myTuple, id:\.0) {...}
Swiftly
  • 152
  • 6
0

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()
    }
}
Jason
  • 2,223
  • 2
  • 20
  • 28
0

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.

Matt Ke
  • 3,599
  • 12
  • 30
  • 49
-1

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, enumerated or indicies hacks. 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)
    }
}
malhal
  • 26,330
  • 7
  • 115
  • 133
  • This is not exactly true. Instead of making NamedFont Identifiable with `var id: String { name }` you could also do `ForEach(namedFonts, id: \.name)`. The "collection of identified data" **doesn't need** to conform to Identifiable as stated in the docs: *Either the collection’s elements must conform to Identifiable or you need to provide an id parameter to the ForEach initializer.* Your way is just a one side of a coin. Also, `id: .\self` is not a hack, although I admit it is used incorrectly many times. – pawello2222 Jul 03 '21 at 22:39
  • I was actually about to suggest the same answer as you. Creating a simple struct here seems like the best and the simplest solution to me. – Hollycene Jul 13 '23 at 17:54