I am currently implementing a UITableViewController with an NSFetchedResultsController. It fetches some objects from CoreData and displays them as rows in one section as expected.
Now, I would like to have one additional section with exactly one row that displays aggregated information about the fetched objects.
From what I know, one NSFetchedResultsController can only have one fetch request, but I would have to use another one to get the aggregated information.
Perhaps I should use one NSFetchedResultsController for the Overall section and another one for the single object section, but this feels kind of strange to me.
What do you think?
You may consider to use one fetchedResultController. And add some observer functions in the delegate of fetchedResultController:
- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller{
[self updateAggregatedInformation : basedOnFecthedObjects]; //with collection keypath methods;
[self updateTableView];
}
There is a little overhead in the tableView DataSource, but gain in performance.
I just solved the problem and it was more easy than I thought.
The NSFetchedResultsControllerDelegate receives messages from the FRC and so the delivered IndexPath.section attributes have be adapted according to the tableView. The same needs to be done the other way around when the tableView calls the FRC to create cells that are backed up by the fetched entities.
Related
I have a collectionView with NSFetchedResultsController. Some cells in the collectionView will have extra embedded UI elements queried from core data, and those extra embedded UI elements also need the update functionality of NSFetchedResultsController.
So my question is, what is the recommended way of approaching this?
Since the number of cells, and whether or not each of them has embedded UI elements depends on the data actually fetched from server, we cannot use sectionKeyPath of NSFRC right?
EDIT: the extra UI elements are not the same model as the embedding cells and thus require separate queries (NSPredicate).
EDIT: Our core data model:
RelationModel
type
status
Relationships(fromProfile, toProfile)
ProfileModel
..many fields
Relationships(photos)
Basically, the extra UI elements will be toProfiles with the embedding cell being the fromProfile. But because there are more than one kind of relations in the app, we decided to have a separate model for relations. And I found it hard to set a relationship from ProfileModel to the RelationModel
A NSFetchedResultsController is a really cool object. It does a fetch and then monitors core-data for changes. While it has an interface that relies on indexPaths so it is natural to think of these indexPath's as the same indexPaths as your collectionView there is no requirement that you do that. The indexPaths of the fetchedResultsController can be different than the indexPaths of the collectionView - you just need to careful about keeping track which indexPaths you are dealing with and translating from one to the other.
For example: You have a set of widgets that you fetching from core data. Some of the widgets have a property of extraWidgetInfo which you want to display in you UI as an extra cell. The fetchedResultsController says that there are 4 element (all in section 0). But the collectionView can display that as
[section1] widget1,
[section2] widget2, widget 2 extra info,
[section3] widget3,
[section4] widget4, widget 4 extra info.
While the fetchedResultsController only says that there are 4 elements, there are 6 cells in the collectionView. You would also have to translate the fetchedResultsController indexPath when dealing with updates. An update would translate to a reload section, and an add would translate to an insertSection and insert of some amount of rows in that section. You could also just call reloadData when core data updates (If you data is updating rarely this may not be a bad option).
Since we require separate queries in each cell, we ended up setting separate NSFetchedResultsController in each cell when it's dequeued. And then have the NSFetchedResultsController set as nil when prepareForReuse.
I have found a fix for this, but I'm not really liking the fix. My issue goes like this. I am using a NSFetchedResultsController to populate a UICollectionView-- which displays a collection of images. Each image is described by a Core Data object (e.g., its file name is in the Core Data object).
I have UI controls that allow a user to delete multiple images at the same time, and was having a problem when the user would delete more than one object. The code to do the deletion was:
for image in images {
CoreData.sessionNamed(CoreDataExtras.sessionName).remove(image)
}
CoreData.sessionNamed(CoreDataExtras.sessionName).saveContext()
(Some of this is my library code).
With the deletion of two objects, I get a crash and the following log message:
CoreData: error: Serious application error. Exception was caught
during Core Data change processing. This is usually a bug within an
observer of NSManagedObjectContextObjectsDidChangeNotification.
Invalid update: invalid number of items in section 0. The number of
items contained in an existing section after the update (99) must be
equal to the number of items contained in that section before the
update (101), plus or minus the number of items inserted or deleted
from that section (0 inserted, 1 deleted) and plus or minus the number
of items moved into or out of that section (0 moved in, 0 moved out).
with userInfo (null)
What fixes the problem is if I change the deletion code to:
for image in images {
CoreData.sessionNamed(CoreDataExtras.sessionName).remove(image)
CoreData.sessionNamed(CoreDataExtras.sessionName).saveContext()
}
I guess the problem is that in the delegate callback method:
- (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject
atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type
newIndexPath:(NSIndexPath *)newIndexPath {
I do:
collectionView.deleteItems(at: [indexPath])
Apparently, you can either do a reloadItems in the didChangeObject method, or you can do a saveContext after each object deletion.
If you delete several images, and then save the context, the FRC processes all the deletions - so its sections, fetchedObjects, etc, reflect all those changes. But it then calls the didChangeObject: delegate method separately for each change. In that method, you call the collectionView update methods (eg. deleteItems); the collectionView then calls its dataSource methods and does a quick tally up: there were X items, Y items were deleted, there are now Z items and throws an error because Z != X-Y.
When a FRC is used with a tableView, this problem is overcome by using the tableView beginUpdates and endUpdates calls in the FRC controllerWillChangeContent: and controllerDidChangeContent: delegate methods. This causes the tableView to defer doing the tally up until ALL the individual changes have been processed - at which point the numbers do add up.
Your solution - to call saveContext after each deletion - causes the FRC to process each deletion in turn: updating its sections, fetchedObjects, etc, to reflect only one deletion at a time. This keeps the FRC's data in sync with the collectionView. One possible refinement would be to call processPendingChanges on the context after each deletion, instead of saving the context. This avoids saving data when you might not want to, but nonetheless causes each deletion to be processed separately.
The alternative is to mimic the tableView's beginUpdates/endUpdates mechanism for holding all the collectionView updates until all the FRC updates have been processed. This works broadly as follows:
Create arrays to keep track of the changes (inserts, deletes).
Each time didChangeObject: is called, add the corresponding indexPath to the relevant array.
When controllerDidChangeContent: is called, iterate through the arrays (deletions first, when inserts) calling the corresponding collectionView update methods. (Then empty the arrays ready for the next batch of updates).
Some good explanations and potential implementations are included in this question and its answers.
I am using a FetchedResultsController to fetch the data for my UITableView. The data is created via actions performed on another tab (so my table may have 5 items but if I switched tabs and go back to my table, it may have more than 5 items that it should display). My table can potentially contain many rows. Right now I am using [myFetchedResultsController performFetch] in my viewDidLoad.It appears that when I create data in my other tab, when I switch back to my table tab, that new data is put into my table automatically without me perfomring [myFetchedResultsController performFetch] again. Here are my questions:
1) Does a fetchedResultsController automatically monitor the manajedObjectContext for changes and fetches the new objects if they come into existence? (This appears to be what is happening but I just want to make sure. Perhaps I have some code that is helping me do this that I forgot I put in somewhere)
2) Does the fetch performed by [myFetchedResultsController performFetch] fetch all of the objects at that time, or does it fetch only what it can fit in the view of the table and it fetches the rest later as it needs it (as you scroll in the table)? I ask because since my table can potentially have a lot of rows, it seems inefficient to fetch all the data at once if only ~12 of them will be displayed on the table at once.
EDIT: I just realized that in my FetchedResultsController delegate methods, I have
- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller
{
[self.tableView reloadData];
}
Am I correct in saying that a fetchedResultsController monitors for change, but will not apply it to the table unless the table is reloaded as I have done? If so, then I have another question about UITableView. Does reloading the table only reload the rows in view and then the other rows are updated as you scroll? Again I ask because if my data is very large, it seems inefficient to reload the entire table if it will reload all rows at once.
Yes, if you add a delegate
You should set the fetch request batch size when you configure the FRC because it can only load an appropriate number of items for the screen if you tell it how many that is.
You apply the changes, the FRC just collects and supplies the data. The delegate method tells you about a change. Reloading affects the whole table in terms of row count but only shows the visible rows (assuming the batch size is set appropriately).
tableView:numberOfRowsInSection is sent to the delegate of a UITableView to find out how many rows it needs to have in a given section.
My question is, when and how often is this method called?
The method is called very first time the tableview is getting loaded and if you are more interested in the delegates then put a breakpoint and check when and where which delegate is called and how many times.
Below are the instances when that function will get called,
For the first time when table is loaded
the time you reload the table data
the time you add/update/delete your row or sections dynamically.
The method - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section is a protocol method of the UITableViewDataSource - protocol. It will be called
the very first time your table view is loaded based on that you have set the dataSource properly, e.g.
self.yourTableView.dataSource = self;
If you are interested in updating your table again at a later time you can call
[self.yourTableView reloadData];
in order to reload the entire table. If you are only interested in reloading a part of your table you can do something similar to
NSIndexSet *reloadSet = [NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, [self numberOfSectionsInTableView:self.yourTableView])];
[self.yourTableView reloadSections:reloadSet withRowAnimation:UITableViewRowAnimationAutomatic];
Hope it helps!
My question is, when and how often is this method called?
Short Answer : When your UITableView needs to update something.
Long Answer : Delegates Methods generally called themselves however it may be called multiple times when your UITableView needs to update something. By default, it's called very first time the tableview is getting loaded or updated (reloaded).
It depends on how often user will scroll UITable view to section and how many sections there are. This value, which is returned by this function and is casched. Method will need be revoked if you will update content of table view (filtering results, or updating data via reloadData).
Best thing for you will be to add logging to this function and check this yourself.
I have a single section table view which is hooked up to a NSFetchedResultsController. When the results from the core data is empty and under some extra conditions I would like to create a custom row which contents will be provided not by CoreData. Is there a clean way of implementing this with the NSFetchedResultsController?
No, NSFetchedResultsController does not offer a feature like that. Just implement your table data source to check how many fetchedObjects the FRC has and use that to decide if the rows of the table should be provided by the FRC or you should instead show your custom empty row.