swiftswiftuiswiftui-foreach

SwiftUI iterating through @State or @Published dictionary with ForEach


Here is a minimum reproducible code of my problem. I have a dictionary of categories and against each category I have different item array. I want to pass the item array from dictionary, as binding to ListRow so that I can observer the change in my ContentView. Xcode shows me this error which is very clear Referencing initializer 'init(_:content:)' on 'ForEach' requires that 'Item' conform to 'Identifiable.

The solution shows in this question Iterating through set with ForEach in SwiftUI not using any @State or @Published variable. They are just using it for showing the data. Any work around 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")]
            }
        }
    }