What is the best practice to ensure changes to the sectionKeyPathName
values (which are from a related entity) are propagated to the UITableView's section headers (includes redrawing the section headers with the new value and changing the section ordering if needed)?
I am under the assumption that the NSFetchedResultsController delegate's current capabilities cannot meet my needs because
NSFetchedResultsController
is observing the one entity (Recipe in my case for the table rows) and not the entity of the sectionKeyPathName
(Category's name property for me.)NSFetchedResultsController
delegate's controller(_:didChange:atSectionIndex:for)
never returns move or update for its type parameterBackground
I have two entities, Category and Recipe, where a Category can have multiple Recipes and a Recipe will belong to only 1 Category (aka 1:M relationship.)
I have a tableview of Recipes, with a section for each Category. The section header is the Category's name. I have used a fetchedResultsController to populate the table view. Everything works as expected (i.e., as Recipes are updated, added, deleted or have their Category changed, the table view reflects those changes) with ONE EXCEPTION. I cannot get the section headers to change when a Category name changes. I want not only the headers to reflect a changed name but to re-sort as needed.
Code Snippets
I'll share as needed. As I said everything else works and there is a lot of code.
FWIW - My sectionKeyPathName
value is the same as the first value of the sortDescriptor array.
Failed Attempt
I assume I need to observe the Category's name
property for changes. Worst case, my sections would need re-ordering. Hence, if the name
changed, I would just performFetch()
again and then reloadData()
on the table. I tried this technique.
NSManagedObjectContextWillSave
to determine if a Category (that was currently a section in my table view) had its name
property change.name
property changed, then when I observed NSManagedObjectContextDidSave
, I would perform the fetch again and reloadData(). This seemed to solve the problem but then crashed when I tested moving a Recipe to a different Category. (The moving test worked prior to this change.) (The crash is An exception was caught from the delegate of NSFetchedResultsController during a call to -controllerDidChangeContent:. Invalid update: invalid number of sections.
)fetchedResultsController are great. But they are not super smart. They monitor changes only for the entity that it is setup to fetch. It assumes that all other changes does not effect it. So if you have a predicate or sort descriptor based on a relationship it can cause problems.
Possible solutions:
Even though a fetchedResultsController is designed to work nicely in a 1:1 setup with a tableview or collectionView (ie the indexPath of the fetchedResultsController is equal to the indexPath of the view) it does not need to work that way.
You can make a fetchedResultsController for the categories with no sectionKeyPathName. Then setup that the number of section is the number of items in the controller. The number of items in each section is equal to the number of recipes in each category.
This can becomes more difficult if you have a predicate to filter the recipes.
If you are changing the category in only a handful of places it may be easy to simply 'dirty' all of the recipes in the category to trigger the fetchedResultsController. If you set a property equal to itself (ie r.recipeId = r.recipeId) it will trigger an update in the fetchedResultsController. So when you change the categoryName also iterate though all of the recipes and dirty them.
FetchedResultsController are pretty light objects and you don't need to be afraid of making as many as you need. You can create two. One for the recipes and one for the categories. The sections are based on the categories fetchedResultsController. For the recipe fetchedResultsController you group it based on a property of the categories that does not change (the categoryId for example). Then when you want to know the numberOfRows for a category you have to find it in the recipe fetchedResultsController. Figuring out the indexPath can be a little annoying - but if you make a separate object to manage it, it is not a big deal.
If you are displaying all recipes I would recommend #1. If you have a predicate on you recipes I would recommend #2. I would only recommend #3 if you are a code purist and want full separation of you model and your views.