I have a paging tableView that loads a new page every time the user over scrolls, sort of like the instagram feed. The reloading of the tableView happens in a callback after the new page was fetched. The code looks something like this:
func fetchNewPage() {
ws.fetchNewPage(completion: { () -> Void in {
updateDataSource()
self.tableView.reloadData()
})
}
At the same time, the cells in the tableView should be updated when the user taps on them (they change their background color). This happens after a call to the WS. The code looks something like this:
func markAsSeen(indexPath: NSIndexPath) {
ws.markAsSeen(completion: { () -> Void in {
self.tableView.reloadRowsAtIndexPaths([indexPath], withRowAnimation: .None)
})
}
My problem is that the dataSource can change the number of elements while the markAsSeen completion block is executing. Then the app crashes with this exception:
Fatal Exception: NSInternalInconsistencyException
Invalid update: invalid number of rows in section 0. The number of rows contained in an existing section after the update (50) must be equal to the number of rows contained in that section before the update (25), plus or minus the number of rows inserted or deleted from that section (1 inserted, 1 deleted) and plus or minus the number of rows moved into or out of that section (0 moved in, 0 moved out).
So, my question is if you have any idea how to synchronize these two blocks so that the app does not crash anymore but also the row gets updated.
Thanks a lot
Your idea about synchronization is correct. As an option you can perform updates of data source on main queue. In such case data source and UI updates will be queued and executed serially, as result change of data will be predictable and consistent.
Also it worth to mention that all UI calls is mandatory should be executed on main thread. From your code it is not really clear that it is true.
Related
I'm trying to delete a collectionView cell using this code :
myDataSourceArray.remove(at:index)
collectionView.performBatchUpdates({
collectionView.deleteItems(at: [indexPath])
}
, completion: nil)
its very straight forward deletion ,and after trying to delete any cell from section 1 for example , it causes this exception :
Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Invalid update: invalid number of items in section 1. The number of items contained in an existing section after the update (2) must be equal to the number of items contained in that section before the update (2), 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).'
my collectionView distribution for cells is each section contains 2 cells like image below
and for last section if its containing 1 cell , it will contain 1 cell
func collectionView(_ collectionView: UICollectionView,
numberOfItemsInSection section: Int) -> Int {
if myDataSourceArray.count%2==1 && myDataSourceArray.count/2 == section {
return 1
}else{
return 2
}
}
What exactly causes this exception ?
Thanks in advance :)
Your problem is likely caused by the fact that you're not handling the deletion from the data source correctly.
The proper way to do it is to do the data source deletion inside of the update block, otherwise you can do the changes outside the update block but make sure the UI matches (reload it) before the update begins (which you did not).
Try moving myDataSourceArray.remove(at:index) inside the update block.
https://developer.apple.com/documentation/uikit/uicollectionview/1618045-performbatchupdates
The other thing which is probably not causing the issue but that you should be handling regardless (that I don't see you doing here), is sections have to be manually deleted. If your deletion operation results in the last cell of a section being deleted, you have to delete the section as well using deleteSections(:).
I'm allowing users to select/deselect cells in my collection view and then hit a save/delete button.
When I'm selecting rows, I add them to a dictionary
var selectedIndexes = Dictionary<IndexPath, String> ()
and when I deselect the row I set the selectedIndexes[indexPath] = nil
When the user hits delete I run
for index in selectedIndexes.keys {
indexesToDelete.append(index)
}
collectionView.deleteItems(at: indexesToDelete)
This goes into the selectedIndexes dictionary, grabs the indexes, adds those indexes into an array of indexes "indexesToDelete", and then after the forloop is over, I'm deleting that array of indexes.
When I run this I get:
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 (5) must be equal to the number of items contained in that section before the update (5), plus or minus the number of items inserted or deleted from that section (0 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've printed everything out and the indexesToDelete are the correct indexes for what I'm trying to delete. I don't fully understand what the error message is saying.
What it is saying is that your collection view is no longer in sync with your data source. So you also need to update whatever you are using as your dataSource to reflect your changes and then call deleteCells.
for index in selectedIndexes.keys {
indexesToDelete.append(index)
}
yourCollectionViewDataSourceArray.remove(at: index)
collectionView.deleteItems(at: indexesToDelete)
if your collectionView data source is yourCollectionViewDataSourceArray, both are synced together. Every time collectionView reloads data it uses the the function with argument numberOfItemsInSection. When it finds out that your array.count is different than the total of items after deletion it gives you this error. Take it as a rule every time you want to update your collectionView by code, update the data array before updating it. Hope that helps
Running through the codelabs Firebase tutorial covering FriendlyChat. Addressed constant issue (answered elsewhere) but when I go to upload the selected image, my app crashes. I redid all steps and tested the "complete" version of the tutorial source code to ensure it wasn't something I was doing. No luck. Anyone else seeing this issue?
Here is the exception...
2016-05-23 17:25:13.119 FriendlyChatSwift[61549:15581893] *** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Invalid update: invalid number of rows in section 0. 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 (6), plus or minus the number of rows inserted or deleted from that section (1 inserted, 0 deleted) and plus or minus the number of rows moved into or out of that section (0 moved in, 0 moved out).'
When I check the message count on initial load, it's fine. When I go to the imagePicker and come back, suddenly message count is 1 but the row count is still 6.
The problem appears to be here:
override func viewWillAppear(animated: Bool) {
self.messages.removeAll()
// Listen for new messages in the Firebase database
_refHandle = self.ref.child("messages").observeEventType(.ChildAdded, withBlock: { (snapshot) -> Void in
self.messages.append(snapshot)
self.clientTable.insertRowsAtIndexPaths([NSIndexPath(forRow: self.messages.count-1, inSection: 0)], withRowAnimation: .Automatic)
})
}
If you remove all the messages then the index is off in the call to insertRowsAtIndexPaths.
I got this to work by moving all the code from viewWillAppear to the end of viewDidLoad. Because viewWillAppear is called every time the view shows up again, it ends up being called after you get out of the photos view and return back to the table view. viewDidLoad, on the other hand, is only called once, at the beginning when the view is loaded. It works also when the user goes back to the home screen and returns to the app.
Figured out the real solution. The discussion on this twitter thread explains what you have to do and why. The code firebase provides should go in viewDidAppear, as they had. However, you need to reload your table after you removeAll messages.
self.messages.removeAll()
self.clientTable.reloadData()
And then your viewWillDisappear needs to correctly remove the observer, as outlined by Ibrahim Ulukaya above.
self.ref.child("messages").removeObserverWithHandle(_refHandle)
The reason it should be in viewDidAppear is so you can re-start observing the table when the view comes back to the top. And the observer needs to be removed in viewWillDisappear so that you do not have a view responding to an observer/notification when it is not on screen as this violates the MVC rules.
Hope this helps. I struggled with this for a while. Not sure how the Firebase team didn't catch this.
The main error was that in viewWillDisappear removed the observer in wrong reference.
It should be
self.ref.child("messages").removeObserverWithHandle(_refHandle)
instead. Also you'd like to reloadData after removeAll in your viewWillAppear.
You wouldn't need to removeobjects and reloaddata everytime if you move them to viewDidLoad and deAlloc.
(I'll be updating the sourcecode shortly.)
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.
I have a table view with each rows having image and some text. The images are loaded asyc. As soon as image is fetched from server, the delegate methods gets called. The delegate method contains the index path which initiated the image fetching, so that I can reload only those cells. I have extra check to make sure that once the image is fetched, the data source contains enough data so that index path doesn't go out of bounds. Despite setting this condition the app crashes.
There is a chance that my table data gets updated before the image is fetched. I know this is cause of the issue, but I am not sure why this is making a crash despite adding a check before
reloadRowsAtIndexPaths?
The error is:
Fatal Exception NSInternalInconsistencyException Invalid update: invalid number of rows in section 2. The number of rows contained in an existing section after the update (4) must be equal to the number of rows contained in that section before the update (6), plus or minus the number of rows inserted or deleted from that section (1 inserted, 1 deleted) and plus or minus the number of rows moved into or out of that section (0 moved in, 0 moved out).
Can some one share any inputs on this?
Code snippet that reloads the table
// this method will be called on main thread
- (void)loadImageAtIndexPath:(NSIndexPath *)inIndexPath {
if(inIndexPath.row < dataSource.count) {
[self.listView reloadRowsAtIndexPaths:[NSArray arrayWithObject:inIndexPath]
withRowAnimation:UITableViewRowAnimationNone];
}
}