WWDC22 SALE: Save 50% on all my Swift books and bundles! >>

SOLVED: Day 38 Issue

Forums > 100 Days of SwiftUI

I am working on sectioning the expenses on personal and business

I am getting the following error: Referencing initializer 'init(_:content:)' on 'ForEach' requires that '[ExpenseItem]' conform to 'Identifiable'

Not sure why because my expense item conforms to identifiable.

struct ExpenseItem: Identifiable, Codable {
    var id = UUID()
    let name: String
    let type: String
    let amount: Double

    init(id: UUID, name: String, type: String, amount: Double) {
        self.id = id
        self.name = name
        self.type = type
        self.amount = amount
    }
}

struct ContentView: View {
    @ObservedObject var expenses: Expenses
    @State private var showAddView = false

    var sectionedData: [[ExpenseItem]] {
        let dictionaryByType = Dictionary(grouping: expenses.items, by: { $0.type })
        return [dictionaryByType["Personal"], dictionaryByType["Business"]].compactMap({ $0 })
    }

    var body: some View {
        NavigationView {
            List {
                ForEach(sectionedData) { section in
                    Section(header: SectionHeaderView(headerName: section[0].type)) {
                        ForEach(section) { expense in
                            ExpenseItemRow(expenseItem: expense)
                        }
                    }
                }
            }
            .navigationTitle("iExpense")
            .toolbar {
                ToolbarItem(placement: .navigationBarTrailing) {
                    HStack {
                        Button(action: { showAddView.toggle() }, label: {
                            Image(systemName: "plus")
                        })

                        EditButton()
                    }
                }
            }
            .sheet(isPresented: $showAddView) {
                AddView(expenses: expenses)
            }
        }
    }

    private func removeItems(at offsets: IndexSet) {
        expenses.items.remove(atOffsets: offsets)
    }
}

So, I am not sure why it thinks it doesn't conform to Identifiable when ExpenseItem does. Can someone explain that to me.

Thanks Taz

   

sectionedData is typed as [[ExpenseItem]], an array of arrays of ExpenseItem.

The items inside an array can be Identifiable but the array itself is not.

I can't test this right now because I'm away from my computer, but you could try something like:

extension Array: Identifiable where Element: Identifiable {
    var id: Element.ID { //or whatever type you want
        //somehow calculate an ID for the array
    }
}

   

Thanks @roosterboy for the help so far,

I will tell ya, I am confused on your suggestion:

I have tried this:

extension Array: Identifiable where Element == ExpenseItem {
    public var id: ExpenseItem.ID {  //Cannot declare a public property in an extension with internal requirements
        first { $0.id == id } //Cannot convert return expression of type 'ExpenseItem?' to return type 'ExpenseItem.ID' (aka 'UUID')
    }
}

I get the following errors: I guess I am just confused on this. Any help here would be appreciated. I am sure I am missing something just not seeing it right now.

Taz

   

@roosterboy

Thanks for your help. I couldn't understand what you were refering to. I would love to learn more about how to make the array itself conform to identifiable.

So, what I ended up doing was making ExpenseItem conform to Hashable and then used .self. Heres the code

struct ExpenseItem: Identifiable, Codable, Hashable {
    var id: UUID
    let name: String
    let type: String
    let amount: Double

    init(id: UUID, name: String, type: String, amount: Double) {
        self.id = id
        self.name = name
        self.type = type
        self.amount = amount
    }
}

struct ContentView: View {
    @ObservedObject var expenses: Expenses
    @State private var showAddView = false

    var sectionedData: [[ExpenseItem]] {
        let dictionaryByType = Dictionary(grouping: expenses.items, by: { $0.type })
        return [dictionaryByType["Personal"], dictionaryByType["Business"]].compactMap({ $0 })
    }

    var body: some View {
        NavigationView {
            List {
                ForEach(sectionedData, id: \.self) { section in
                    Section(header: SectionHeaderView(headerName: section[0].type)) {
                        ForEach(section) { expense in
                            ExpenseItemRow(expenseItem: expense)
                        }
                        .onDelete(perform: { offsets in removeItems(at: offsets, in: section) })
                    }
                }
            }
            .navigationTitle("iExpense")
            .toolbar {
                ToolbarItem(placement: .navigationBarTrailing) {
                    HStack {
                        Button(action: { showAddView.toggle() }, label: {
                            Image(systemName: "plus")
                        })

                        EditButton()
                    }
                }
            }
            .sheet(isPresented: $showAddView) {
                AddView(expenses: expenses)
            }
        }
    }

    private func removeItems(at offsets: IndexSet, in expanseItems: [ExpenseItem]) {
        let expensesToDelete = offsets.map({ expanseItems[$0] })
        for _ in expensesToDelete {
            expenses.items.remove(atOffsets: offsets)
        }
    }
}

Now it shows them sectioned out. It also deletes them as well. If you feel up to it, as I said I would like to understand better your suggestion. I just couldn't get it to work.

Thanks again Taz

   

extension Array: Identifiable where Element == ExpenseItem {
    public var id: ExpenseItem.ID {  //Cannot declare a public property in an extension with internal requirements
        first { $0.id == id } //Cannot convert return expression of type 'ExpenseItem?' to return type 'ExpenseItem.ID' (aka 'UUID')
    }
}

Inside the id computed variable, you want to calculate something to identify the array. Here you are instead taking the first element that matches some id that is never supplied. And then trying to return that element, which doesn't match the type of the id property. That's why it doesn't work.

Instead, try this (I'm using Int as the id type rather than Element.ID for reasons that should become clear below):

extension Array: Identifiable where Element: Identifiable {
    public var id: Int {
        var h = Hasher()
        forEach { h.combine($0.id) }
        return h.finalize()
    }
}

This takes the id property of each element in the array and constructs a hashed value (which is an Int) from them to use as the id of the array itself.

Here's an example using Array<Int> (note that I had to conform Int to Identifiable for purposes of this example):

extension Int: Identifiable {
    public var id: Int { self }
}

let aInt = [1,2,3,4,5,6,7,8,9,10]
let bInt = [1,2,3,4,5,6,7,8,9,11]
print(aInt.id) //4175619739819157220
print(bInt.id) //4465001197246827190

Note, too, that the id property of the array is guaranteed to be the same within the execution of your app (as long as the input values are the same) but not guaranteed to be the same across executions, so don't store that id for reuse.

   

Thanks,

I will give that a try, i don't think i would have gotten that had you not explained it.

Thanks,

   

I think the way you resolved it is probably a better solution.

   

It's definitely a much simpler solution ans simple is in my opinion easier to maintain. Thanks again for your help. I think I'll stick with what I got.

   

Save 50% in my Black Friday sale.

SAVE 50% To celebrate WWDC22, all our books and bundles are half price, so you can take your Swift knowledge further without spending big! Get the Swift Power Pack to build your iOS career faster, get the Swift Platform Pack to builds apps for macOS, watchOS, and beyond, or get the Swift Plus Pack to learn advanced design patterns, testing skills, and more.

Save 50% on all our books and bundles!

Sponsor Hacking with Swift and reach the world's largest Swift community!

Reply to this topic…

You need to create an account or log in to reply.

All interactions here are governed by our code of conduct.

 
Unknown user

You are not logged in

Log in or create account
 

Link copied to your pasteboard.