MLQ
MLQ

Reputation: 13511

How to build subsections in an NSFetchedResultsController

I'm building an expense tracker where an Expense can belong to only one Category but can have multiple Tags. This is my object graph:

enter image description here

In the screen where I list all the expenses in a table view, I want the expenses to be grouped by date (the sectionDate), and then by Category (or, using a segmented control, by Tag). This is the intended UI:

enter image description here

I can already make an NSFetchedResultsController query all expenses, section them by date, then by category, but I can't get the (1) total for the category and (2) the list of expenses in it. How might I proceed to do that? This is my current code:

let fetchedResultsController: NSFetchedResultsController<NSFetchRequestResult> = {
    let fetchRequest = NSFetchRequest<NSFetchRequestResult>(entityName: "Expense")
    fetchRequest.resultType = .dictionaryResultType

    fetchRequest.sortDescriptors = [
        NSSortDescriptor(key: #keyPath(Expense.sectionDate), ascending: false)
    ]

    fetchRequest.propertiesToFetch = [
        #keyPath(Expense.sectionDate),
        #keyPath(Expense.category)
    ]
    fetchRequest.propertiesToGroupBy = [
        #keyPath(Expense.sectionDate),
        #keyPath(Expense.category)
    ]

    let fetchedResultsController = NSFetchedResultsController(fetchRequest: fetchRequest,
                                                              managedObjectContext: Global.coreDataStack.viewContext,
                                                              sectionNameKeyPath: #keyPath(Expense.sectionDate),
                                                              cacheName: nil)
    return fetchedResultsController
}()

Upvotes: 3

Views: 373

Answers (2)

MLQ
MLQ

Reputation: 13511

I appreciate @pbasdf's answer, but I feel that I'll have a hard time wrapping my head around the solution after a long time of not looking at the code.

What I've come around to doing is instead of fetching Expense objects, I defined a new entity for the subsections themselves (CategoryGroup, and I will also make a TagGroup) and fetch those entities instead. These entities have references to the Expense objects that they contain, and the Category or the Tag that represents the group. This is my (partially complete) data model:

enter image description here

And my NSFetchedResultsController is now far simpler in code:

let fetchedResultsController: NSFetchedResultsController<CategoryGroup> = {
    let fetchRequest = NSFetchRequest<CategoryGroup>(entityName: "CategoryGroup")
    fetchRequest.sortDescriptors = [
        NSSortDescriptor(key: #keyPath(CategoryGroup.sectionDate), ascending: false)
    ]
    return NSFetchedResultsController(fetchRequest: fetchRequest,
                                      managedObjectContext: Global.coreDataStack.viewContext,
                                      sectionNameKeyPath: #keyPath(CategoryGroup.sectionDate),
                                      cacheName: "CacheName")
}()

The downside is that I now have to write extra code to make absolutely sure that the relationships among the entities are correctly defined whenever an Expense or a Category is created/updated/deleted, but that's an acceptable tradeoff for me as it is easier to comprehend in code.

Upvotes: 0

pbasdf
pbasdf

Reputation: 21536

A should forewarn that I've never done this, but personally I would set about it as follows:

  1. Don't use propertiesToGroupBy: it forces you to use the .dictionaryResultType which means you can only access the underlying managed objects by executing a separate fetch.
  2. Instead, add another computed property to the relevant NSManagedObject subclass, combining the sectionDate and the category.name. This property will be used as the sectionNameKeyPath for the FRC, so that the FRC will establish a section in the tableView for each unique combination of sectionDate and category name.
  3. Add category.name as another sort descriptor for the fetch underlying the FRC. This will ensure that the Expense objects fetched by the FRC are in the correct order (ie. all Expense objects with the same sectionDate and category name are together).
  4. Add a section header view for each section. The name property for the section (from the FRC) will include both the sectionDate and the category name. In most cases, you can strip out and ignore the sectionDate, displaying only the category name and corresponding total (see below). But for the very first section, and indeed the first section for any given sectionDate, add an additional view (to the section header view) showing the sectionDate and overall total for that sectionDate.
  5. Working out whether a given section is the first section for the sectionDate is a little tricky. You could retrieve the name for the previous section and compare the sectionDates.
  6. To collapse/expand the sections, maintain an array holding the collapsed/expanded state of each section. If the section is collapsed, return 0 in the tableView numberOfRowsInSection datasource method; if expanded use the figure provided by the FRC.
  7. For the category totals, iterate through the objects array for the relevant section (or use a suitable .reduce to achieve the same).
  8. For the sectionDate totals, filter the fetchedObjects for the FRC to include only the Expense objects for the relevant sectionDate, and then iterate or .reduce the filtered array.

I am happy to add or amend if any of that needs clarification.

Upvotes: 1

Related Questions