Invalid tableview update when rotating device - ios

I am using what I believe is a common pattern for adding items to a table view -
main controller creates modal controller and registers itself as delegate
modal view controller is presented
user provides some data and hits save button in the modal's navigation bar
modal view controller send its delegate a message containing the details entered
original controller receives the message and dismisses the modal
original controller updates the data model and inserts a new row into its tableview
This is working well except in one specific scenario.
If the device is rotated while the modal is presented, the app crashes after dismissing the modal. The new row is inserted correctly but then immediately afterwards fails:
*** Assertion failure in -[UITableView _endCellAnimationsWithContext:],
/SourceCache/UIKit_Sim/UIKit-2380.17/UITableView.m:1070
2013-07-28 17:28:36.404 NoHitterAlerts[36541:c07] *** 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 (8) must be equal to the number of rows contained in that section before the update (8), 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).'
I can't for the life of my figure out why this occurs. Test cases that work consistently fail if I rotate with the modal presented. I've noticed that simply reloading the tableview instead of inserting the row with animation works fine.
Here is a bare bones project that demonstrates the same issue:
demo project
Run the project in the iPhone sim
Add an item to the list - works fine
Back on first screen, rotate to landscape
Run the same test again. Still works.
Back on first screen, launch modal. Rotate simulator while modal is still presented. Hit 'Add Item'. Crashes.
Any ideas on what might be happening here?

I see what your problem is. In your MainController's -modalController:didAddItem: method, you are first adding the object to the self.arrayOfStrings, and not inserting the row into the tableView until after the -dismissViewControllerAnimated method has completed.
This appears to work when the orientation is not changed when the modalViewController is open, however if you do change the orientation, the mainController's orientation isn't changed until it is being closed. Once this happens, it seems that the tableView's data is automatically reloaded due to the frame being changed.
Thus, because the arrayOfStrings is having the object added before the animation starts and the -insertRowsAtIndexPaths:withRowAnimation: is not called until after the animation is completed, the table view thinks it has already gotten the rows by the time the insert method is reached.
In order to fix this, all you have to do is move your method that adds the string to the array into the completion block right before you call the insertRows method on your tableView.
So your method would end up looking something like the following with whatever changes you need for your actual project:
- (void)modalController:(ModalController *)controller didAddItem:(NSString *)string
{
//dismiss the modal and add a row at the correct location
[self dismissViewControllerAnimated:YES completion:^{
[self.arrayOfStrings addObject:string];
[self.tableView insertRowsAtIndexPaths:#[[NSIndexPath indexPathForRow:self.arrayOfStrings.count - 1 inSection:0]] withRowAnimation:UITableViewRowAnimationAutomatic];
}];
}

Related

Image Upload Crash in Firebase Codelabs FriendlyChat Tutorial

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.)

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.

iOS: Is it possible to override the show-red-delete-button-method when swiping a table view cell?

I have a problem with the "delete-table-view-cell-with-left-swipe"-feature.
Situation:
I have a table view which content is handled by a background thread. From time to time a pop up windows occurs with the info, that a new element for the table view is available. Hit "yes" to add it, hit "no" to discard it. To delete an element later i use the mentioned "delete-table-view-cell-with-left-swipe"-feature. Everything works fine.
Problem:
One special constellation causes an app crash. When i swipe to the left the red delete-button becomes visible. In that moment the background thread fires the pop up window. I click "yes" to add the element to the table view. The pop up windows disappears and i click on the delete-button. CRASH!
Crash-Report:
Assertion failure in -[UITableView _endCellAnimationsWithContext:], /SourceCache/UIKit/UIKit-3347.44/UITableView.m:1623
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 (1),
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).'
First throw call stack:
(0x1864cc2d8 0x197cf00e4 0x1864cc198 0x187380ed4 0x18b0f2ec8 0x10007fcc4 0x18b10cf10 0x18b2028ec 0x18af41404 0x18af2a4e0 0x18af40da0 0x18aefffc0 0x18af3a898 0x18af39f50 0x18af0d18c 0x18b1ae324 0x18af0b6a0 0x186484240 0x1864834e4 0x186481594 0x1863ad2d4 0x18fbc36fc 0x18af72fac 0x100106cb8 0x19836ea08)
libc++abi.dylib: terminating with uncaught exception of type NSException**
Reason:
Obviously the reason for the crash is the inconsistency of the number of rows in the table view. When starting the delete-procedure 1 element is in the list. After clicking the delete-button there should be no more (0) element in the list. Due to the upcoming new element during the delete-procedure there is instead one element left when the delete-procedure ends. I think that causes the app crash.
Solution:
In fact the solution of the problem is not that difficult. The easiest way would be to block new upcoming elements while the delete-procedure is in progress.
And that is my question: How can i do this? Is there an easy way to recognize when a table view cell is swiped to the left and the delete-procedure is started? To recognize the delete-button-click is easy of course, but not what i need. I found an other solution which is to implement an own SwipableTableViewCellClass which can handle the swipe. But a much easier solution would be nice.
Something like:
Override a the method which is called when the cell is swiped to the
left
Stop the background thread from firing pop up windows
Call the normal routine to make the red delete button visible
Has anybody an idea how i can solve my problem?
Thx in advance!
EDIT:
Method to handle delete-button-click looks like this:
// Override to support editing the table view.
- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath {
if (editingStyle == UITableViewCellEditingStyleDelete) {
[tableView beginUpdates];
[tableView deleteRowsAtIndexPaths:[NSArray arrayWithObjects:indexPath, nil] withRowAnimation:UITableViewRowAnimationFade];
[tableView endUpdates];
}
}
I believe you can implement the following UITableViewDelegate methods to detect the start and end.
// The willBegin/didEnd methods are called whenever the 'editing' property is
// automatically changed by the table (allowing insert/delete/move). This is
// done by a swipe activating a single row
- (void)tableView:(UITableView*)tableView willBeginEditingRowAtIndexPath:(NSIndexPath *)indexPath;
- (void)tableView:(UITableView*)tableView didEndEditingRowAtIndexPath:(NSIndexPath *)indexPath;
Simply create a flag to mark that the delete is in progress and use it to prevent or reschedule the popup.

NSFetchedResultsController conflict with number of changed records

I have an iPad app with ARC, storyboards, supporting ios5+.
I have a VC that is working with NSFetchedResultsController on its own.
The VC has two tableviews pointing at two different entities.
I have single record add, change, delete, and moving of rows working fine on this VC.
I've now introduced a new feature to enable the user to quick add multiple rows to these tableviews.
I do this by providing a segue (modal) from this VC to a new VC (lets call it quickAddVC).
The user enters the data on quickAddVC, and then I segue back (modal) to the original VC.
I save the new records (via an alternate call) before (I believe) setting up the NSFetchedResultsControllers for the VC again (I set them to nil in ViewDidUnload).
However I keep getting this mismatch error.
Somehow it keeps registering the number of inserts that occur from the quickAddVC as 1 insert no matter how many records I add, and it somehow remembers how many records were in the NSFetchedResultsControllers before my segues to do this comparison.
Error:
* Assertion failure in -[UITableView _endCellAnimationsWithContext:], /SourceCache/UIKit/UIKit-2372/UITableView.m:1070
Failure in didChangeObject, name=NSInternalInconsistencyException reason=Invalid update: invalid number of rows in section 0. The number of rows contained in an existing section after the update (5) must be equal to the number of rows contained in that section before the update (2), 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).
Any ideas on what I am doing wrong or should try would be useful.
I can't figure out how to get the NSFetchedResultsControllers to simply forget its original state and restart from scratch which would I think resolve this issue for me.
I haven't posted any of the code, I guess I could post a lot if needed but hoping there is enough here to outline the design issue and get some ideas.
Just reposting the answer to mark question as closed.
Ok solved the issue. Basically I was clearing the NSFetchedResultsControllers in the wrong place. By setting them to nil just prior to the segue all works fine. I'd made an incorrect assumption that given the ViewDidLoad method was being called when I segued back again, that the ViewDidUnload method was also being called but that was not the case.

numberOfSectionsInTableView is requested in/after viewDidLoad ONLY if tableview is empty

A simple tableviewController, empty. A modal that can be launched from a button. No data in the data source for the tableview, and no rows displayed.
I open the modal, use it to add an item, and return to the tableview controller. The tableview updates itself automatically, and the new row is displayed.
I add a second item. The table view does NOT update automatically.
I can tell by logging inside numberOfSectionsInTableView that even if I go to add the first item and cancel, the tableview refreshes - it asks for the number of sections, rows, and the cell to display (if there is one). But not if there is one pre-existing row in the table.
I've scoured my code but can't find anything that would cause the tableview to know when the first item is added, but none afterwards.
Any ideas?
EDIT - I have further narrowed my issue so will close this shortly and have posted a more specific question at Why does an empty tableView check the number of sections but a non-empty one does not?
from Apple's documentation:
UITableView overrides the layoutSubviews method of UIView so that it
calls reloadData only when you create a new instance of UITableView or
when you assign a new data source.
maybe you are setting data source of table view after showing that modal? So at the second time data source does not changes and tableView does not update.
another key (I'm not sure about that) may be the showing of the modal first time. The method -addSubview for modal is causing -layoutSubviews.

Resources