Group Core Data items by category in List in SwiftUI

I have a Core Data container with two entities, a Category and an Item. The Item can have one Category assigned and the Category can be assigned to many Items.

What I need to do is group the items by category in a list in SwiftUI.

The code below doesn't group all items by category, it only shows one item by category. How can I group all items that have the same category assigned under the same category group?

Core Data Entities

Category
    Attributes
        name
    Relationship
        items (Type: To Many)

Item
    Attributes
        name
    Relationship
        category (Type: To One)

Swiftui

struct ItemsView: View {
    let selectedList:List

    @EnvironmentObject private var itemSM: ItemServiceModel
    
    var body: some View {
        List {
            ForEach(itemSM.items) { item in
                Section(header: Text(item.category?.name ?? "")) {
                    ForEach(itemSM.items.filter { $0.category == item.category }) { filteredItem in
                        Text("\(filteredItem.name ?? "")")
                    }
                }
            }
        }
        .onAppear{
           itemSM.loadItems(forList: selectedList)
       }
    }
}

Service Item Service Model

class ItemServiceModel: ObservableObject{
    let manager: CoreDataManager
    @Published var items: [Item] = []

    func loadItems(forList list: List){
        let request = NSFetchRequest<Item>(entityName: "Item")
        
        let sort = NSSortDescriptor(keyPath: \Item.name, ascending: true)
        request.sortDescriptors = [sort]
        
        let filter = NSPredicate(format: "list == %@", list)
        request.predicate = filter

        do{
            items =  try manager.context.fetch(request)
        }catch let error{
            print("Error fetching items. \(error.localizedDescription)")
        }
    }
}

This is what I see, as you can see, only one Fruits & Vegetables section should exist.

We don't see any screen capture here.

Not sure I understand what you are doing:

1.         List {
2.             ForEach(itemSM.items) { item in
3.                 Section(header: Text(item.category?.name ?? "")) {
4.                     ForEach(itemSM.items.filter { $0.category == item.category }) { filteredItem in
5.                         Text("\(filteredItem.name ?? "")")
6.                     }
7.                 }
8.             }
9.         }

You create a new section for each item.

So you will probably create several sections with the same name.

A solution:

  • create a computed var, an array containing all the different categories

private var categories : [String] { // Create all the categories (only once of course) }

  • replace line 2 to 5 by
            ForEach(categories) { catagory in
                Section(header: Text(catagory)) {
                    ForEach(itemSM.items.filter { $0.category == category }) { filteredItem in
                        Text("\(filteredItem.name ?? "")")

Here what worked for me. It basically returns and displays all categories and their respective items from the specified list.

CategoryServiceModel

func loadCategories(for list: List) -> ([Category], [Item]) {
    let request = NSFetchRequest<Category>(entityName: "Category")

    let predicate = NSPredicate(format: "ANY items IN %@", list.items ?? [])
    request.predicate = predicate

    let sort = NSSortDescriptor(keyPath: \Category.name, ascending: true)
    request.sortDescriptors = [sort]

    do {
        let categories = try manager.context.fetch(request)
        var categories: [Category] = []
        var items: [Item] = []

        for category in categories {
            categories.append(category)
            if let items = category.items?.allObjects as? [Item] {
                let filteredItems = items.filter { $0.list == list }
                items.append(contentsOf: filteredItems)
            }
        }

        return (categories, items)
    } catch let error {
        print("Error fetching categories. \(error.localizedDescription)")
        return ([], [])
    }
}

SwiftUI

  List {
      ForEach(categorySM.categories) { category in
          Section(header:Text(category.name ?? "")){
              ForEach(category.items?.allObjects as? [Item] ?? []) { item in
                  Text("\(item.name ?? "")")
              }
          }
      }
  }
   .onAppear{
           categorySM.loadCategories(for: selectedList)
    }

Thanks for feedback.

So, it works correctly now ? If so, don't forget to close the thread by marking my answer as correct. Otherwise please explain what's the remaining issue.

Good continuation.

Group Core Data items by category in List in SwiftUI
 
 
Q