Swift - invalid update of collection view - multiple data changes - ios

I have problem with my collection view and reloading/inserting items. I have my items array in view controller which contains UICollectionView. I change content of items array in many places. Sometimes I insert new items and sometimes I must reload items again from database.
Not all the time but sometimes I get "classic" error when inserting new items:
Invalid update: invalid number of items in section 0. The number of
items contained in an existing section after the update (16) must be
equal to the number of items contained in that section before the
update (16), plus or minus the number of items inserted or deleted from
that section (1 inserted, 0 deleted) and plus or minus the number of
items moved into or out of that section (0 moved in, 0 moved out).
I know meaning of this error but It's really hard to fix this because as I wrote I can get notifications and then I reload data immediately (get items from database and reload collection view) and it could happen when inserting new item and similar conflicts could happen.
So I am not sure what is best solution for this. Should I lock array and make it thread safe (but I think I edit it mostly in main thread) or should I somehow "lock" collection and when there is insert operation then there couldn't be changes to items or collection view couldn't be reloaded? Or is there other solution?
Bonus: Is it possible that app wouldn't crash when error above happens and just full reload collection view? Propably not but it could help a lot.

Related

iOS UITableView - crash when inserting new section with error message which may indicate that I should call moveSection:toSection: method

I reload, insert and delete table view's rows and sections in a single begin/end update block. I do not call moveSection:toSection: nor moveRowAtIndexPath:toIndexPath: method.
I am aware that deletion and reloading of existing cells are being executed first, regardless of the order of method calls in update block. I do not insert rows for the new section that is inserted. Also, I am updating data source before table view update block.
Deletion and reloading work just fine for me, but I have a problem with inserting new sections. I got error messages of type:
Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Invalid update: invalid number of rows in section 5. The number of rows contained in an existing section after the update (1) must be equal to the number of rows contained in that section before the update (3), plus or minus the number of rows inserted or deleted from that section (0 inserted, 0 deleted) and plus or minus the number of rows moved into or out of that section (0 moved in, 0 moved out).'
It is interesting that I do not delete or reload any table view's rows and sections in the corresponding table view update block. Also, there is 1 row in all sections that come before section 5. New section was inserted at index 0. So it seems to me that I might be missing moveSection:toSection: method call..?
I couldn't find more complex examples of simultaneous inserting, deleting and reloading rows and sections in a single update block on Internet, so I would really appreciate any suggestions and links to the examples that could guide me how to handle more complex table view updates.

How to solve this CollectionView crash?

My app has two CollectionViewControllers. Only one is visible at a given time.
I have created the following structure on storyboard: two container views on top of each other. Every container view has a CollectionViewController embedded. The visibility of a particular container view determines which collectionViewController is visible.
This is the problem. Both CollectionViewControllers are receiving data in parallel but iOS has a bug that will make the app crash if one CollectionViewController tries to execute an insert using performBatchUpdates while it is invisible.
Trying to prevent that, I have created a BOOL flag on both CollectionViewControllers so they can know if they are visible and execute or not the performBatchUpdates. Something like:
if (self.isThisCollectionViewVisible == NO) return;
[self.collectionView performBatchUpdates:^{
// bla bla... perform insert,m remove...
This solves part of the problem. But the app continues to crash on the following condition: if I tap the button to switch to the invisible CollectionViewController making it visible while it is receiving updates.
I mean this: lets call A the first CollectionViewController and B the second one. A is visible and B is invisible at this point. B starts receiving data and is trying to do a performBatchUpdates but as it is invisible, the if (self.isThisCollectionViewVisible == NO) return; is preventing performBatchUpdates to run, what is fine. Now I make A invisible and B visible. At this point the flag self.isThisCollectionViewVisible is set to YES and performBatchUpdates makes the app crash with this error:
* Assertion failure in -[CollectionView _endItemAnimationsWithInvalidationContext:tentativelyForReordering:], /BuildRoot/Library/Caches/com.apple.xbs/Sources/UIKit/UIKit-3512.60.7/UICollectionView.m:4625
* Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Invalid update: invalid
number of items in section 0. The number of items contained in an
existing section after the update (76) must be equal to the number of
items contained in that section before the update (70), plus or minus
the number of items inserted or deleted from that section (5 inserted,
2 deleted) and plus or minus the number of items moved into or out of
that section (0 moved in, 0 moved out).'
I think the CollectionViewController is really not yet ready and updated to be able to do a performBatchUpdates... and this is not a matter of not updating the data source previously because it is being updated.
What checks can I do to prevent that from happening?
NOTE: I noticed something strange about this crash in particular. It says that 5 elements are being inserted and 2 deleted but in fact 3 elements are being inserted, 0 deleted and 2 changed when the crashes happen.
For me adding self.collectionView.numberOfItemsInSection(0) fixed the crash.
The collectionView has issues while inserting items when it is not visible.
Seems like I'm not alone with my solution: http://www.openradar.me/15262692
This crash told you that you didn't updated your datasource for collection. You need to update your dataSource (array or dictionary) and reload collection view data after you perform performBatchUpdates.
Invalid update: invalid number of items in section 0. The number of items contained in an existing section after the update (76) must be equal to the number of items contained in that section before the update (70), plus or minus the number of items inserted or deleted from that section (5 inserted, 2 deleted) and plus or minus the number of items moved into or out of that section (0 moved in, 0 moved out).'
As written in apple docs
Deletes are processed before inserts in batch operations. This means
the indexes for the deletions are processed relative to the indexes of
the collection view’s state before the batch operation, and the
indexes for the insertions are processed relative to the indexes of
the state after all the deletions in the batch operation.
So, move the changes before the inserts and it will the trick!
Encountered the same error today, for me, in performBatchUpdates block replace this:
NSArray *selectedItemsIndexPaths = [self.collectionView indexPathsForSelectedItems];
with this:
NSIndexPath *selectedIndexPath = [NSIndexPath indexPathForRow:self.selectIndex inSection:0];
NSArray *selectedItemsIndexPaths = #[selectedIndexPath];
Maintain the index by myself, it's OK now. The error should not be associated with data source, if you have had update the data source. It maybe related to the reuse of cells.

Infinite UITableView Scroll with NSFetchedResultsController

I'm trying to use this method to implement an infinitely scrolling UITableView
The core logic of the solution is:
To increase the tableview content by a factor of 3, so that we make the 3 copies of the content laid one after another vertically.
Whenever the top end of the scroll is reached, move the the scroll offset back to start of the 2nd copy
When the bottom end of the scroll is reached, we move the scroll offset back to the start of the 2nd copy minus the height of the tableview, so that we end up showing the same content as we are now.
This means that a single cell insert or delete actually results in three inserts or deletes. Because my table's datasource is populated by an NSFetchedResultsController it causes an Assertion Failure.
The number of rows contained in an existing section after the update (12) must be equal to the number of rows contained in that section before the update (15), plus or minus the number of rows inserted or deleted from that section (0 inserted, 1 deleted) and plus or minus the number of rows moved into or out of that section (0 moved in, 0 moved out).
Is there some way to stop the program from crashing in these cases? I'd really appreciate any help/pointers. Thanks.
This can be kind of tricky in iOS especially when you're first starting out. The key is to update the datasource that is backing your UITableView before you inform the tableview of changes.
For instance, if you have an NSArray backing your tableview, then you will want to remove or add items to it, prior to calling reloadSection: on the tableview.
Although it is the least optimized solution, while your testing feel free to simply call reloadData which will ignore what the tableview has cached and force it to recalculate based on whatever is backing the tableview.
You can use NSFetchedResultsControllerDelegate method controller:didChangeObject:atIndexPath:forChangeType:newIndexPath:. To insert\delete 3 objects.
To keep it simple you could return 3 for number of sections in your table view (this will handle getting 3 copies of objects). And then in your NSFetchedResultsControllerDelegate update objects in all sections of your UITableView (insert, delete or move). You should use row number from the NSFetchedResultsControllerDelegate and apply all changes 3 times each for different section. Remember to keep it consistent with UITableView data source method tableView:numberOfRowsInSection:.

Interesting issue with UICollectionView reloadData

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.

UITableView and CoreDate "Invalid update: invalid number of rows in section" After returning from another view and Saving

I have a UITableView that is backed by core data with sections grouped by date. I can delete and add while I'm on the that view just fine. The NSFetchController and the UITableView stay in sync just fine. But then I drill down to an individual object, make changes, save and return. I don't add from the other view, but sometimes delete, most of the the time I'm just making edits to existing records, and none of the edits would cause them to change which section they belonged to. At that point if I try to delete a row I get:
Assertion failure in -[UITableView _endCellAnimationsWithContext:], /SourceCache/UIKit_Sim/UIKit-1448.89/UITableView.m:974
2011-10-01 14:55:42.691 Lotus Bud[8073:fa03] 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 (1) must be equal to the number of sections contained in the table view before the update (1), plus or minus the number of sections inserted or deleted (0 inserted, 1 deleted). with userInfo (null)
What do I need to do to get them back in sync after saving from another view.
I would suggest listening for the NSManagedObjectContextDidSaveNotification and breaking in the debugger when it is fired. Then you can see what section is being deleted and hopefully backtrace to the source of the problem.

Resources