How to create subsections in NSFetchedResultsController
I am creating an expense tracer where Expense
can only belong to one Category
, but can have multiple Tag
s. This is my graph object:
In the screen where I list all expenses in a table, I want the expenses to be grouped by date ( sectionDate
) and then Category
(or, using a segmented control, by Tag
). This is the intended user interface:
I can already NSFetchedResultsController
query all expenses, divide them by date, then by category, but I cannot get (1) the total for the category and (2) the list of expenses in it. How can i do this? 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
}()
source to share
I appreciate @ pbasdf's answer, but I feel like I'm going to have a hard time wrap my head around a solution after a long time without looking at the code.
Instead of fetching objects Expense
, instead of fetching objects Expense
, I have defined a new object for the subsections themselves ( CategoryGroup
and I will also do TagGroup
) and select those entities. These objects have references to the objects Expense
they contain and Category
or Tag
that represent the group. This is my (partially complete) data model:
And mine is NSFetchedResultsController
now much 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 additional code to absolutely make sure that the relationships between entities are correctly defined whenever created or updated / deleted Expense
or Category
, but an acceptable tradeoff for me, how is it easier to understand in code.
source to share
A must be warned that I have never done this, but personally I would say about it this way:
- Don't use
propertiesToGroupBy
: It forces you to use.dictionaryResultType
, which means you can only access the underlying managed objects by doing a separate fetch. - Instead, add another computed property to the appropriate NSManagedObject subclass by concatenating
sectionDate
andcategory.name
. This property will be usedsectionNameKeyPath
for both FRC, so FRC will set a section in tableView for each unique combination of section name and category. - Add
category.name
as another sort descriptor for the selection underpinning FRC. This ensures that the objectsExpense
retrieved by FRC are in the correct order (i.e. All objectsExpense
with the same section name and category name together). - Add a section heading title for each section. The property
name
for a section (from FRC) will include both the section name and the category name. In most cases, you can cut and ignore the sectionDate, displaying only the category name and the corresponding total (see below). But for the very first section, and even for the first section, for any given Date section, add an additional view (to the section header view) that shows the sectionDate and the total for that section Date. - Determining whether a given section is the first section for a section is a bit tricky. You can get the name for the previous section and compare sectionDates.
- To collapse / expand sections, maintain an array containing the compressed / expanded state of each section. If the section is collapsed, return 0 in the tableView
numberOfRowsInSection
datasource method ; if extended usage is used by a figure provided by FRC. - For category totals, iterate through the array
objects
for the appropriate section (or use a suitable one.reduce
to achieve the same). - For sectionDate totals, filter
fetchedObjects
for FRC to include only those objectsExpense
for the matchingsectionDate
, then iterate or .reduce the filtered array.
I am happy to add or amend if any of them need clarification.
source to share