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.
Related
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.
Description
I have a CoreData entry called Person, I fetch it using NSFetchedResultsController, with a fetchRequest ordered by property "name". Then I display the "name" in the table view cell.
Problem
When I change the entity's "name" property and the rows reorder, NSFetchedResultsControllerDelegate does give me a NSFetchedResultsChangeType.move. But the "name" displayed on the cell is outdated, meaning I'm not receiving NSFetchedResultsChangeType.update
The Table View Programming Guide: Inserting and Deleting Rows and Sections says batch updates do updates first, then deletions, lastly insertions.
It defers any insertions of rows or sections until after it has handled the deletions of rows or sections. The table view behaves the same way with reloading methods called inside an update blockāthe reload takes place with respect to the indexes of rows and sections before the animation block is executed. This behavior happens regardless of the ordering of the insertion, deletion, and reloading method calls.
Question
Any idea on how the notifications sent by NSFetchedResultsController are implemented, specifically on the ordering of insert, delete, update & move?
Or how can I use some kind of code to efficiently (meaning partial update, not reload all data) solve this particular problem?
Your setup is non-standard. The sectionNameKeyPath is really meant for sections not rows. Fetch the Person entity and populate the cell with a person's name directly via itemForRowAtIndexPath.
You will then have the expected change types available.
You certainly got this error:
CoreData: error: Serious application error. An exception was caught
from the delegate of NSFetchedResultsController during a call to
-controllerDidChangeContent:. Invalid update: invalid number of sections. The number of sections contained in the table view after
the update (8) must be equal to the number of sections contained in
the table view before the update (8), plus or minus the number of
sections inserted or deleted (1 inserted, 0 deleted). with userInfo
(null)
This message just doesn't make sense to me as CoreData is related to NSFetchedResultsController BUT CoreData is not related to the table view. (Correct me if I'm wrong)
The way I understand it, the only link between tableview and NSFetchedResultsController is in NSFetchedResultsController's delegate, when we use controller(didChangeObject:) and controller(didChangeSection:)
How is it possible that the iOS framework knows about the tableView number of sections when it's not even mandatory (It's recommended but not mandatory) to use a UITableView...
It should only be able to check the NSFetchedResultsController result fetchedObjects result array and not in the tableview.
Typically, the controller's delegate is implemented in a following way (taken from Apple's Core Data Programming Guide):
- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller
{
[[self tableView] endUpdates];
}
When the tableView gets endUpdates message, it does two things that are important in our context:
Counts how many sections were inserted and / or deleted since beginUpdates (by using insertSections:withRowAnimation method)
Checks if these changes were reflected in the data source. In your case, they were not: there was 1 section inserted by calling insertSections:withRowAnimation on the table view, but the data source still returns 8 when asked about section count. That's basically what the error message is saying.
So the table view figures out that its state is inconsistent and it throws and exception.
And we're still in the controllerDidChangeContent delegate method. Let's take a look on the error message again:
An exception was caught from the delegate of
NSFetchedResultsController during a call to
-controllerDidChangeContent
So NSFetchedResultsController calls controllerDidChangeContent on its delegate inside a try block and catches the exception. Then it re-throws it.
Long story short: NSFetchedResultsController knows nothing about the table view - it just caught the exception which happened to be thrown in endUpdates method in UITableView.
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).
When dealing with a UICollectionView in my app I've ran into a strange problem related to reloading data. After lot's of debugging and analyzing logs I've come to the conclusion that if reloadData is immediately followed by insertItemsAtIndexPaths the dreaded error below is guaranteed to occur:
Name: NSInternalInconsistencyException Reason: Invalid update: invalid
number of items in section 0. The number of items contained in an
existing section after the update (1) must be equal to the number of
items contained in that section before the update (1), plus or minus
the number of items inserted or deleted from that section (1 inserted)
...
They only way for this to happen consistently is that internally the UICollectionView is still busy with reloadData when the call to insertItemsAtIndexPaths arrives. The fact that "collectionView:numberOfItemsInSection" is called twice in a row before the insertItemsAtIndexPaths completes seems to support this since that method is never called twice in a row in call other cases.
Has anyone seen similar behavior or can confirm my analysis or even suggest a proper workaround?
Update: Any yes I've made sure that all relevant invocations occur on the main thread.
Update 2: Since the reasoning behind getting into this situation at all has been questioned: I'm using Monotouch and the code in question is intended to keep generic .Net Collections firing this event into the appropriate calls to keep the UICollectionView bound to the collection in sync. When the source collection is cleared it reacts with a Reset action, followed by one or more Add actions when items get inserted into it which leads to the problem outlined above. Hope this helps.
When you call insertItemsAtIndexPaths (removeItemsAtIndexPaths is analogous), you are telling your collectionview that its datasource now has more items available, and that it should insert these available items at the indexpaths you specify.
It checks your datasource whether that statement is true, and if it detects that the amount of old items plus the amount of items you say you inserted is not equal to the amount of new items, it says it can't do the update, as you lied about how many items you changed.
Now, what you are doing is you are telling your collectionview is that it should reload all its data from its datasource (with the new data) and right after that, you tell it you inserted x items. That is a false statement, as you just reloaded the collectionview, which updated its amount of items, and thus the amount of items before the update is equal to the amount of items after the update (you're not doing anything), and not increased by the amount of indexpaths you specified.
I hope you're still with me, because here's your solution:
Remove the reloadData before the insertItemsAtIndexPaths, as this breaks its assertions and will throw exceptions when incorrectly used. If you would like to reload the collectionview before inserting items, make sure you perform insertItemsAtIndexPaths right after you change the items in the datasource.
Read up on the documentation for this method here.