Search code examples
swiftuiswiftui-foreachidentifiable

SwiftUI - Iterate through a @State or @Published dictionary with ForEach


I have an array of Strings, "categories", and associated to each category I have an "Item" struct in another array. With these I have formed a dictionary of type [String: [Item]].

I want to pass the item array from the dictionary as a Binding to ListRow so that changes are observed by the ContentView.

Xcode shows me this error:

Referencing initializer 'init(_:content:)' on 'ForEach' requires that 'Item' conform to 'Identifiable.

The solution from this question, Iterating through set with ForEach in SwiftUI, does not use any @State or @Published variables. They are just using them for showing the data. Any workaround for this issue?

struct Item {
    var id = UUID().uuidString
    var name: String
}


struct ListRow {
    
    @Binding var item: Item
    
    var body: some View {
        TextField("Place Holder", text: $item.name)
    }
}

struct ContentView: View {
    
    var categories = ["Bakery","Fruits & Vagetables", "Meat & poultry", "Dairy & Eggs", "Pantry", "Household"]
    @State private var testDictionary: [String: [Item]] = [:]
    
    var body: some View {
        
        VStack(alignment: .leading) {
            
            ForEach(categories, id: \.self) { category in
                Text(category)
                    .font(.system(size: 30))
                ForEach(testDictionary[category]) { item in
                    ListRow(item: item)
                }
            }
            
        }.onAppear(
        addDummyDateIntoDictonary()
        )
    }
    
    func addDummyDateIntoDictonary() {
        for category in categories {
            testDictionary[category] = [Item(name: category + "1"), Item(name: category + "2")]
        }
    }
}

Solution

  • One problem is that you didn't make ListRow conform to View.

    //           add this
    //            ╭─┴──╮
    struct ListRow: View {
        @Binding var item: Item
    
        var body: some View {
            TextField("Place Holder", text: $item.name)
        }
    }
    

    Now let's address your main problem.

    A Binding is two-way: SwiftUI can use it to get a value, and SwiftUI can use it to modify a value. In your case, you need a Binding that updates an Item stored somewhere in testDictionary.

    You can create such a Binding “by hand” using Binding.init(get:set:) inside the inner ForEach.

    struct ContentView: View {
    
        var categories = ["Bakery","Fruits & Vagetables", "Meat & poultry", "Dairy & Eggs", "Pantry", "Household"]
        @State private var testDictionary: [String: [Item]] = [:]
    
        var body: some View {
            VStack(alignment: .leading) {
                ForEach(categories, id: \.self) { category in
                    Text(category)
                        .font(.system(size: 30))
                    let items = testDictionary[category] ?? []
                    ForEach(items, id: \.id) { item in
                        let itemBinding = Binding<Item>(
                            get: { item },
                            set: {
                                if
                                    let items = testDictionary[category],
                                    let i = items.firstIndex(where: { $0.id == item.id })
                                {
                                    testDictionary[category]?[i] = $0
                                }
                            }
                        )
                        ListRow(item: itemBinding)
                    }
                }
            }.onAppear {
                addDummyDateIntoDictonary()
            }
        }
    
        func addDummyDateIntoDictonary() {
            for category in categories {
                testDictionary[category] = [Item(name: category + "1"), Item(name: category + "2")]
            }
        }
    }