Assertion failure in -[UITableView _endCellAnimationsWithContext: - ios

I'm an amateur at best, and stuck on this error! surely something simple...
- (void)addTapped:(id)sender {
TechToolboxDoc *newDoc = [[TechToolboxDoc alloc] initWithTitle:#"New Item" thumbImage:nil fullImage:nil];
[_itemArray addObject:newDoc];
//[self.tableView beginUpdates];
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:_itemArray.count-1 inSection:0];
NSArray *indexPaths = [NSArray arrayWithObject:indexPath];
NSLog(#"%#",indexPath);
[self.tableView insertRowsAtIndexPaths:indexPaths withRowAnimation:YES];
[self.tableView selectRowAtIndexPath:indexPath animated:YES scrollPosition:UITableViewScrollPositionMiddle];
[self performSegueWithIdentifier:#"MySegue" sender:self];
//[self.tableView endUpdates];
it is breaking on the line the says
[self.tableView insertRowsAtIndexPaths:indexPaths withRowAnimation:YES];

You need to add [UITableView beginUpdates] and [UITableView endUpdates] around:
[self.tableView insertRowsAtIndexPaths:indexPaths withRowAnimation:YES];
From the class reference:
Note the behavior of this method when it is called in an animation
block defined by the beginUpdates and endUpdates methods. UITableView
defers any insertions of rows or sections until after it has handled
the deletions of rows or sections. This happens regardless of ordering
of the insertion and deletion method calls. This is unlike inserting
or removing an item in a mutable array, where the operation can affect
the array index used for the successive insertion or removal
operation. For more on this subject, see Batch Insertion, Deletion,
and Reloading of Rows and Sections in Table View Programming Guide for
iOS.

I think you are inserting the row in your tableView but not updating the number of rows at section that's why you are getting error in this line. So along with inserting the row You should also increase the array count or whatever you are using to show the number of rows in table view.

In my case, I was using Parse and I deleted a few users (one of which was my iPhone simulator). I got this error whenever refreshing the tableView.
I just deleted the app from the Simulator and made a new account and it works flawlessly.
#droppy's answer helped give me the lightbulb moment of what was wrong!
Hope that helps someone.

Related

Dynamically changing data source causing deleteRowsAtIndexPaths:indexes to crash

Tearing my hair out trying to get this to work. I want to perform [self.tableView deleteRowsAtIndexPaths:indexes withRowAnimation:UITableViewRowAnimationLeft];,
More detailed code of how I delete:
int index = (int)[self.messages indexOfObject:self.messageToDelete];
[self.messages removeObject:self.messageToDelete];
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:index inSection:0];
NSArray *indexes = [[NSArray alloc] initWithObjects:indexPath, nil];
[self.tableView deleteRowsAtIndexPaths:indexes withRowAnimation:UITableViewRowAnimationLeft];
This works fine however if I get a push notification (i.e a new message received) whilst deleting the app will crash and display an error like:
Assertion failure in -[UITableView _endCellAnimationsWithContext:],
/SourceCache/UIKit/UIKit-3347.44/UITableView.m:1327
2015-07-04 19:12:48.623 myapp[319:24083] *** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason:
'attempt to delete row 1 from section 0 which only contains 1 rows
before the update'
I suspect this is because my data source is changing, the size of the array that
-(NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
references while deleting won't be consistent because it was incremented by one when the push notification triggered a refresh. Is there any way I can work around this? Am I correct that deleteRowsAtIndexPaths uses the numberOfRowsInSection method?
So, in order to solve your problem you need to ensure that your data source will not change while some table view animations are in place. I would propose to do the following.
First, create two arrays: messagesToDelete and messagesToInsert. These will hold information about which messages you want to delete/insert.
Second, add a Boolean property updatingTable to your table view data source.
Third, add the following functions:
-(void)updateTableIfPossible {
if (!updatingTable) {
updatingTable = [self updateTableViewWithNewUpdates];
}
}
-(BOOL)updateTableViewWithNewUpdates {
if ((messagesToDelete.count == 0)&&(messagesToInsert.count==0)) {
return false;
}
NSMutableArray *indexPathsForMessagesThatNeedDelete = [[NSMutableArray alloc] init];
NSMutableArray *indexPathsForMessagesThatNeedInsert = [[NSMutableArray alloc] init];
// for deletion you need to use original messages to ensure
// that you get correct index paths if there are multiple rows to delete
NSMutableArray *oldMessages = [self.messages copy];
for (id message in messagesToDelete) {
int index = (int)[self.oldMessages indexOfObject:message];
[self.messages removeObject:message];
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:index inSection:0];
[indexPathsForMessagesThatNeedDelete addObject:indexPath];
}
for (id message in messagesToInsert) {
[self.messages insertObject:message atIndex:0];
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:0 inSection:0];
[indexPathsForMessagesThatNeedInsert addObject:indexPath];
}
[messagesToDelete removeAllObjects];
[messagesToInsert removeAllObjects];
// at this point your messages array contains
// all messages which should be displayed at CURRENT time
// now do the following
[CATransaction begin];
[CATransaction setCompletionBlock:^{
updatingTable = NO;
[self updateTableIfPossible];
}];
[tableView beginUpdates];
[tableView deleteRowsAtIndexPaths:indexPathsForMessagesThatNeedDelete withRowAnimation:UITableViewRowAnimationLeft];
[tableView insertRowsAtIndexPaths:indexPathsForMessagesThatNeedInsert withRowAnimation:UITableViewRowAnimationLeft];
[tableView endUpdates];
[CATransaction commit];
return true;
}
Lastly, you need to have the following code in all functions which want to add/delete rows.
To add message
[self.messagesToInsert addObject:message];
[self updateTableIfPossible];
To delete message
[self.messagesToDelete addObject:message];
[self updateTableIfPossible];
What this code does is ensures stability of your data source. Whenever there is a change you add the messages that need to be inserted/deleted into arrays (messagesToDelete and messagesToDelete). You then call a function updateTableIfPossible which will update the table view's data source (and will animate the change) provided that there is no current animation in progress. If there is an animation in progress it will do nothing at this stage.
However, because we have added a completion
[CATransaction setCompletionBlock:^{
updatingTable = NO;
[self updateTableIfPossible];
}];
at the end of the animations our data source will check if there are any new changes that need to be applied to the table view and, if so, it will update the animation.
This is a much safer way of updating your data source. Please let me know if it works for you.
Delete a Row
int index = (int)[self.messages indexOfObject:self.messageToDelete];
[self.messages removeObject:self.messageToDelete];
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:index inSection:0];
[tableView beginUpdates];
[tableView deleteRowsAtIndexPaths:#[indexPath] withRowAnimation:UITableViewRowAnimationLeft];
[tableView endUpdates];
Delete a Section
Note : if your TableView have multiple section than you have to delete whole section when section contain only one row instead of deleting row
int index = (int)[self.messages indexOfObject:self.messageToDelete];
[self.messages removeObject:self.messageToDelete];
NSIndexPath *indexPath = [NSIndexSet indexSetWithIndex:0];
[tableView beginUpdates];
[tableView deleteSections:#[indexPath] withRowAnimation:UITableViewRowAnimationLeft];
[tableView endUpdates];
I have a few test about your code above. And you cannot do that at the same time the least we can do is something like:
//This will wait for `deleteRowsAtIndexPaths:indexes` before the `setCompletionBlock `
//
[CATransaction begin];
[CATransaction setCompletionBlock:^{
[tableView reloadData];
}];
[tableView beginUpdates];
[tableView deleteRowsAtIndexPaths:indexes withRowAnimation:UITableViewRowAnimationLeft];
[tableView endUpdates];
[CATransaction commit];
In your code whe have:
/*
int index = (int)[self.messages indexOfObject:self.messageToDelete];
[self.messages removeObject:self.messageToDelete];
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:index inSection:0];
NSArray *indexes = [[NSArray alloc] initWithObjects:indexPath, nil];
[self.tableView deleteRowsAtIndexPaths:indexes withRowAnimation:UITableViewRowAnimationLeft];
*/
This is not safe because if if the notification is triggered and for some reason [self.tableView reloadData]; called right before deleteRowsAtIndexPaths that will cause an crash, because the tableView is currently updating the datas then interrupt by the deleteRowsAtIndexPaths:, try this sequence to check:
/*
...
[self.tableView reloadData];
[self.tableView deleteRowsAtIndexPaths:indexes withRowAnimation:UITableViewRowAnimationLeft];
This will cause a crash...
*/
Hmm.. Back to your code, Let's have a simulation that could cause the crash.. THIS is just an assumption though so this is not 100% sure (99%) only. :)
Let assume self.messageToDelete is equal to nil;
int index = (int)[self.messages indexOfObject: nil];
// since you casted this to (int) with would be 0, so `index = 0`
[self.messages removeObject: nil];
// self.messages will remove nil object which is non existing therefore
// self.messages is not updated/altered/changed
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:index inSection:0];
// indexPath.row == 0
NSArray *indexes = [[NSArray alloc] initWithObjects:indexPath, nil];
[self.tableView deleteRowsAtIndexPaths:indexes withRowAnimation:UITableViewRowAnimationLeft];
// then you attempted to delete index indexPath at row 0 in section 0
// but the datasource is not updated meaning this will crash..
//
// Same thing will happen if `self.messageToDelete` is not existing in your `self.messages `
My suggestion is check the self.messageToDelete first:
if ([self.messages containsObject:self.messageToDelete])
{
// your delete code...
}
Hope this is helpful, Cheers! ;)
'attempt to delete row 1 from section 0 which only contains 1 rows before the update'
This says your index path, during deletion, references row 1. However row 1 doesn't exist. Only row 0 exists (like the section, they are zero-based).
So how are you getting an indexPath one greater than the number of elements?
Can we see your notification handler that inserts a row? You could do some hack where if the animation is in process, you performSelector:withDelay: during the notification processing to allow time for the animation to complete.
In you code, the reason for crash is : you are updating array but not update data source of UITableView. so your dataSource also needs to reflect the changes by the time endUpdates is called.
So as per Apple Documentation.
ManageInsertDeleteRow
To animate a batch insertion, deletion, and reloading of rows and sections, call the corresponding methods within an animation block defined by successive calls to beginUpdates and endUpdates. If you don’t call the insertion, deletion, and reloading methods within this block, row and section indexes may be invalid. Calls to beginUpdates and endUpdates can be nested; all indexes are treated as if there were only the outer update block.
At the conclusion of a block—that is, after endUpdates returns—the table view queries its data source and delegate as usual for row and section data. Thus the collection objects backing the table view should be updated to reflect the new or removed rows or sections.
Example:
[tableView beginUpdates];
[tableView deleteRowsAtIndexPaths:deleteIndexPaths withRowAnimation:UITableViewRowAnimationFade];
[tableView endUpdates];
Hope this help you.
I think the answer is alluded to in many of the comments here, but I'll try and tease it out a bit.
The first problem is that it doesn't look like you are bracketing your deletion method call in the beginUpdates/endUpdates methods. This is the first thing I would fix, otherwise as warned in Apple's documentation things can go wrong. (That doesn't mean they will go wrong, you're just safer doing it this way.)
Once you've done that, the important thing is that calling endUpdates checks the data source to make sure that any insertions or deletions are accounted for by calling numberOfRows. For example, if you delete a row (as you are doing), when you call endUpdates you best be sure that you've also deleted an item from the data source. It looks like you are doing that part right because you are removing an item from self.messages, which I assume is your data source.
(Incidentally you are correct that calling deleteRows... on its own without bracketing in beginUpdates/endUpdates also calls numberOfRows on the data source, which you can easily check by putting a break point in it. But again, don't do this, always use beginUpdates/endUpdates.)
Now it is possible that between calling deleteRows... and endUpdates someone else is modifying the self.messages array by adding an object to it, but I don't know if I really believe that because it would have to be extremely unfortunately timed. It's far more likely that when you receive a push message you aren't handling the insertion of the row properly, again by not using beginUpdates/endUpdates, or by doing something else wrong. It would be helpful if you could post the part of the code that handles the insertion.
If that part looks OK then it does look like you are very unfortunately making changes to the self.messages array on a different thread while the deletion code is being called. You can check this by adding a log line where your insertion code adds a message to the array:
NSLog(#"Running on %# thread", [NSThread currentThread]);
or just putting a break point in it and seeing which thread you end up on. If that is indeed the problem, you can dispatch the code that modifies the array and inserts the row onto the main thread (where it should be anyway since it's a UI operation) by doing something like below:
dispatch_async(dispatch_get_main_queue(), ^{
[self.messages addObject:newMessage];
[self.tableView beginUpdates];
[self.tableView insertRows...]
[self.tableView endUpdates];
});
That will ensure that the messages array won't get mutated by another thread while the main thread is busy deleting the row.

How to get last cell index path in iOS?

I am building an IOS application where I am using tableView. Now when I reached to last cell I load +10 data from localDB.
After fetching the data I reloaded the tableView that I don't need in place of reload I want to used updated tableView. for that SOF suggested me below code.
[[self tableView] beginUpdates];
[[self tableView] reloadRowsAtIndexPaths:____ withRowAnimation:UITableViewRowAnimationAutomatic];
[[self tableView] endUpdates];
I don't what should be there in reloadRowsAtIndexPaths value. Please can some help me in understanding this above line of code.
You can get the indexPath of the last row in last section like this.
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:(numberOfRowsInLastSection - 1) inSection:(numberOfSections - 1)];
Here, numberOfSections is the value you return from numberOfSectionsInTableView: method. And, numberOfRowsInLastSection is the value you return from numberOfRowsInSection: method for the last section in the table view.
You don't need to reload cells, because there are no cells after your 10th cell. You want your users to see more cells when they get to the last one as in a news app. The solution is to inset more rows below, here is how:
1- create the rows you want to insert
NSMutableArray *newRows = #[[NSIndexPath indexPathForRow:10 inSection:0]];
3- update your data source array
2- [self.tableView insertRowsAtIndexPaths: newRows, UITableViewRowAnimationAutomatic];

UITableView - using reloadRowsAtIndexPath

I have aUITableView with delegate methods declared. ThenumberOfRows &numberOfSections are updated dynamically.
(NSInteger)tableView:(UITableView *)tableView
numberOfRowsInSection:(NSInteger)section{}
(NSInteger)numberOfSectionsInTableView:
Instead of using the [tableView reloadData], I would need to usereloadRowsAtIndexPath.
However I get a crash with invalid number of rows. How do I handle the dataSource changes? Lets says I have an array of count 10, and when I reload theUITableView usingUIRefreshControls by pulling, and the data from the server. From 10, if the count of array drops to 1, how will I handlereloadRowsAtIndexPath?
[self.tableView reloadRowsAtIndexPaths:[self.tableView indexPathForVisibleRows] withRowAnimation:UITableViewRowAnimationNone]
There seems to be a crash when this is called. How would I handle change in data array's count?
To reload specific cells of Tableview the method is reloadRowsAtIndexPaths: not reloadRowsAtIndexPath
[self.tableview reloadRowsAtIndexPaths:[self.tableview indexPathsForVisibleRows] withRowAnimation:UITableViewRowAnimationNone];
However for reloadRowsAtIndexPaths to work properly datasource needs to be consistant. As Indexpaths are already loaded.
If you still use this method, You will get either arrayIndexOutofBound error or duplicate values in cells(depending on increase or decrease in datasource).
//call begin and end updates while performing some hard core actions on tableview
[My_Table beginupdates];
[My_Table reloadRowsAtIndexPaths:[My_Table indexPathsForVisibleRows] withRowAnimation:UITableViewRowAnimationNone];
[My_Table endupdates];
hope it will helps you.
you can try with this.
[tblMe beginUpdates];
[tblMe reloadRowsAtIndexPaths:[NSArray arrayWithObjects:[NSIndexPath indexPathForRow:0 inSection:0], nil] withRowAnimation:UITableViewRowAnimationNone];
[tblMe endUpdates];
Hope this may help !!!

tableView beginUpdates / endUpdates block reloading entire table even when empty

I have a tableView that periodically gets updated by data coming from a remote service. I load the data, and store the indexPaths of various data that I just inserted, deleted or updated. I then have a begin/endUpdates block that completes the necessary operations, as seen below.
[self.tableView beginUpdates];
if (pathsToDelete) {
[self.tableView deleteRowsAtIndexPaths:pathsToDelete withRowAnimation:UITableViewRowAnimationNone];
}
if (pathsToRefresh) {
[self.tableView reloadRowsAtIndexPaths:pathsToRefresh withRowAnimation:UITableViewRowAnimationNone];
}
[self.tableView insertRowsAtIndexPaths:indexPathsToAdd withRowAnimation:UITableViewRowAnimationNone];
[self.tableView endUpdates];
I noticed recently that cells were being reloaded way more than I was expecting. Following through in the debugger, I noted that in cases where a single record was being refreshed (ie pathsToRefresh was the only one that had anything, and it had only a single index) the entire table was actually reloading. By that, I mean I was seeing requests to the dataSource for each and every cell in the table.
In debugging, I started stripping things down. I got all the way to the point where I only had
[self.tableView beginUpdates];
[self.tableView endUpdates];
And even then, I was seeing the entire table reload (ie cellForRowAtIndexPath was fired for every value in the datasource). So I assumed something else was happening that I'd missed..but removing those two methods resulted in the reload stopping.
Shouldn't begin/endUpdates do nothing in cases where nothing is within the block? I'm at a loss on what is actually happening here, as the doc indicates all they do is bookend table operations - and in this case I have none.
objective c iOS reloadRowsAtIndexPaths , reloadSections
if([self.tableView numberOfRowsInSection:([self.tableView numberOfSections]-1)]-1 == -1){
row = 0;
}else{
row = [self.tableView numberOfRowsInSection:([self.tableView numberOfSections]-1)]-1 ;
}
NSIndexPath* indexPath = [NSIndexPath indexPathForRow: (row) inSection: ([self.tableView numberOfSections]-1)];
// [self.tableView reloadData];
[self.tableView beginUpdates];
[self.tableView reloadSections:[NSIndexSet indexSetWithIndex:0] withRowAnimation:UITableViewRowAnimationAutomatic];
[self.tableView reloadRowsAtIndexPaths:#[indexPath] withRowAnimation:UITableViewRowAnimationNone];
[self.tableView endUpdates];
NSLog(#”Number of Sections %ld”, (long)[self.tableView numberOfSections]);
NSLog(#”Number of Rows %ld”, [self.tableView numberOfRowsInSection:([self.tableView numberOfSections]-1)]-1);

Row deletion does not refresh table view in ios app

I have spent hours searching for the solution with out any luck. I am trying to delete a row (also deselect same row) programmatically. After row deletion call below, UITableViewDelgate methods get called expectedly and data source is updated but UITableView is not refreshed. deselectRowAtIndexPath call also does not work. I tried all kinds of scenarios as shown by commented lines.
Here is my code:
checkoutPerson is called as a result of observer listening for NSNotificationCenter messages.
- (void) checkoutPerson: (NSNumber*) personId {
Person *person = [_people objectForKey:personId];
if( person )
{
// Remove person from data source
int rowIndex = person.rowIndex;
S2Log(#"Deleting row number=%d", rowIndex);
[_allKeys removeObjectAtIndex:rowIndex];
[_people removeObjectForKey: personId];
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:rowIndex inSection:0];
//[[self tableView] beginUpdates];
[self.tableView deselectRowAtIndexPath:indexPath animated:YES];
S2Log(#"Deleting indexPath row=%d", [indexPath row]);
[self.tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath]
withRowAnimation:UITableViewRowAnimationFade];
//[[self tableView] endUpdates];
S2Log(#"Reloading data");
//[[self tableView] reloadData];
//[self performSelector:#selector(refreshView) withObject:nil afterDelay:1.5];
//[self.tableView performSelectorOnMainThread:#selector(reloadData) withObject:nil waitUntilDone:YES];
}
}
I will appreciate for help.
Thanks
-Virendra
I believe deleted cell is not being recycled. If I delete row in the middle, last row is always erased (since there is one less item) but the deleted row remains.
Use the above code between two function for table view
[tableView beginUpdates];
// the deletion code from data source and UITableView
[tableView endUpdates];
By calling this functions you are telling UITableView that you are about to make updates for deleting your cell.
Edit
The other problem I see with your code is you first delete the data from the data source.
Now you are asking for the UITableViewCell (which actually reloads the UITableView)
and then you are deleting the row from UITableView
I guess you should fetch the UITableViewCell before deleting values from your data source.
I found the problem. It has nothing to do with the code I posted above. It is syncing problem between visual display and the contents of data source. I have an embedded UITableView as part of a composite view. In composite view's controller, I was wiring up UITableView's delegate and data source to an instance of UITableViewController. Instead of this, I should have set UITableViewController's tableView property to the embedded UITableView. It seems that UITableView has to be contained within UITableViewController in order to correctly sync up table view visual display to the contents of data source. This also fixes row deselection and scrolling. I also needed to delay reloadData call in which case deleteRowsAtIndexPaths:withRowAnimation is not required. All you need is to modify the contents of your data source and call reloadData with a delay of 1.5 Seconds.
Thanks to all for great help.

Resources