Why does reloadRowsAtIndexPaths not work for iOS 5.0? - ios

SOLVED: See my answer (and possible explanation) below.
I'm making an app that works on iOS 5.1 devices, but not on iOS 5.0 devices. Here is the trouble code that works on 5.1 but NOT on 5.0:
- (void) expandIndexPath: (NSIndexPath *) indexPath afterDelay: (BOOL) delay
{
NSIndexPath *oldSelectedIndexPath = [NSIndexPath indexPathForRow:self.mySelectedIndex inSection:0];
self.mySelectedIndex= indexPath.row;
// [self.myTableView beginUpdates];
[self.myTableView reloadRowsAtIndexPaths:[NSArray arrayWithObjects:[NSIndexPath indexPathForRow:self.mySelectedIndex inSection:0], oldSelectedIndexPath,nil] withRowAnimation:UITableViewRowAnimationNone];
// [self.myTableView endUpdates];
}
Even more curiously, it works in iOS 5.0 if I replace the reloadRowsAtIndexPaths line with [self.myTableView reloadData];. Why is this? Did 5.0 have a bug regarding the reloadRowsAtIndexPaths line? I've tried it on 5.0 with and without begin/endUpdates lines and neither works.
EDIT: To be more specific, when I run the app on iOS 5 it crashes with the following error:
* Assertion failure in -[_UITableViewUpdateSupport _computeRowUpdates], /SourceCache/UIKit/UIKit-1912.3/UITableViewSupport.m:386
[Switching to process 7171 thread 0x1c03]
Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Invalid table view
update. The application has requested an update to the table view
that is inconsistent with the state provided by the data source.'
EDIT: Here are my UITableViewDataSource methods.
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
//I am positive that this will ALWAYS return the number (it never changes)
return self.myCellControllers.count;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
//I cache my table view cells, thus each time this method gets called it will
// return the exact same cell. Yes, I know that most of the time I should be dequeing
// and reusing cells; just trust me that this time, it's best for me to cache them
// (There are very few cells so it doesn't really matter)
CellController *controller = [self.myCellControllers objectAtIndex:indexPath.row];
return controller.myCell;
}
- numberOfSectionsInTableView: always returns 1;
The only interesting method is
- (CGFloat)tableView:(UITableView *)aTableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
if(indexPath.row == self.mySelectedIndex)
{
return DEFAULT_CELL_HEIGHT + self.expandSize;
}
else
{
return DEFAULT_CELL_HEIGHT;
}
}
Just as a quick overview of how this part of the program works, when a user taps a cell, the cell scrolls to the top of the screen. After it is at the top of the screen it's index gets set as self.mySelectedIndex and it expands (gets taller).

I found a solution that works. If I change the expandIndexPath method to this it works:
- (void) expandIndexPath: (NSIndexPath *) indexPath afterDelay: (BOOL) delay {
[self.myTableView beginUpdates];
NSIndexPath *oldSelectedIndexPath = [NSIndexPath indexPathForRow:self.mySelectedIndex inSection:0];
self.mySelectedIndex= indexPath.row;
if(oldSelectedIndexPath.row >= 0)
{
[self.myTableView reloadRowsAtIndexPaths:[NSArray arrayWithObjects:[NSIndexPath indexPathForRow:self.mySelectedIndex inSection:0],oldSelectedIndexPath, nil] withRowAnimation:UITableViewRowAnimationNone];
}
else
{
[self.myTableView reloadRowsAtIndexPaths:[NSArray arrayWithObjects:[NSIndexPath indexPathForRow:self.mySelectedIndex inSection:0],nil] withRowAnimation:UITableViewRowAnimationNone];
}
[self.myTableView endUpdates];
}
As far as I can tell, the problem was that oldSelectedIndexPath was sometimes being set with a negative row value. Thus when I tried to reload an indexPath with a negative row, it crashed in iOS 5.0. It appears that iOS 5.1 fixes this and does more error checking.

Related

TableView: How to proceed after catching 'NSInternalInconsistencyException'?

I have an application with a UITableView. The application communicates with a server.
Problem is the following:
Client 1 deletes a cell of the TableView. The data update is transmitted to the server which sends a data update to Client 2. In the same moment Client 2 deletes a cell from the TableView. Due to the receiving update an NSInternalInconsistencyException is thrown because the number of cells before and after are not as expected (difference is 2 not 1).
The app crashes in
[tableView endUpdates];
Of course I can catch the exception with
- (void) tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath {
if (editingStyle == UITableViewCellEditingStyleDelete) {
#try{
// Send data update to the server
[sender sendDataToServer:data];
// Update tableview
[tableView beginUpdates];
[tableView deleteRowsAtIndexPaths:[NSArray arrayWithObjects:indexPath, nil] withRowAnimation:UITableViewRowAnimationFade];
[tableView endUpdates];
}
#catch (NSException * e) {
// Hide red delete-cell-button
tableView.editing = NO;
// Jump to previous ViewController
[self.navigationController popViewControllerAnimated:YES];
}
}
}
But after the successful catch I get an
EXC_BAD_ACCESS exception in [_UITableViewUpdateSupport dealloc].
What can I do to prevent the app from crashing? What do I have to do in the catch-block to "clean" the situation? Reloading the tableview doesn't take any effect.
EDIT: numberOfRows-method looks like this:
- (NSInteger) tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return [dataSourceArray count];
}
#synchronized(lock) solved my problem.
I just had to make sure that only one thread is able to manipulate the data source array at the same time. Now everything works fine, no more 'NSInternalInconsistencyException'.
Thx to trojanfoe and luk2302.

Cells not removed from UITableView after endUpdates until reloadData

I'm experiencing pretty mysterious glitch:
After endUpdates was called all removed updated cells becoming just hidden and new cell will created, without reusing existing ones.
Usually even -tableView:didEndDisplayingCell:forRowAtIndexPath: not called, but not always.
Result looks like this:
I have no any clue about reasons of this behavior and will appreciate any ideas how to debug and fix this.
Unfortunately, source code it pretty complex and I'm unable to extract something intergal. if you ask me about related code - i will copy it to there.
- (UITableViewCell *)tableView:(UITableView *)tableView
cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
id item = [self.presenter.mediator itemAtIndexPath:indexPath];
NSString *reuseIdentifier = [self.reuseIdentifierMatcher reuseIdentifierForItem:item];
UITableViewCell<ViewItemProtocol>* cell = [tableView dequeueReusableCellWithIdentifier:reuseIdentifier
forIndexPath:indexPath];
cell.item = item;
return cell;
}
- (void)applyChangesSet:(TableChangesSet*)changes
{
LOG_VALUE(#([[NSThread currentThread] isMainThread]));
[self.tableView beginUpdates];
for (TableChange *change in changes.changes)
{
[self applyChange:change];
}
[self.tableView endUpdates];
}
applyChange does not contains any UI manipulations, except deleteRowsAtIndexPaths/insertRowsAtIndexPaths/reloadRowsAtIndexPaths;
Debug output is #([[NSThread currentThread] isMainThread]) is 1

UITableViewCell animation issue in iOS 8

I am trying to expand a UITableViewCell when tapped on it. I use the reload sections method in tableview did select row and change the height of the cell in Height for row at indexpath . It was working fine ,until i tested my app in iOS8 yesterday . For some reason the animation has become quite jaggy in iOS 8 .Please find below the code im using .
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
if(self.selectedOrder.section == indexPath.section) {
self.shouldRevealCell = NO;
self.selectedOrder = [NSIndexPath indexPathForRow:-1 inSection:-1];
}
else {
self.lastSelectedOrder = self.selectedOrder;
self.shouldRevealCell = YES;
self.selectedOrder = indexPath;
}
[self.orderDockTableView beginUpdates];
[self.orderDockTableView reloadSections:[NSIndexSet indexSetWithIndex:indexPath.section] withRowAnimation:UITableViewRowAnimationFade];
[self.orderDockTableView endUpdates];
if(indexPath.section == ([self.orderObjectsArray count] -1)) {
[self.orderDockTableView scrollToRowAtIndexPath:indexPath atScrollPosition:UITableViewScrollPositionBottom animated:YES];
}
}
// Height For Row at index path
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
if(indexPath.section == self.selectedOrder.section) {
return 133.0f;
}
return 85.0f;
}
Im using two images ..one for the collapsed state and one for the expanded state.One more thing i have done is applying a corner radius to my UITableViewCell to make it look like rounded rect ,not sure if it is causing the problem but the animation in iOS8 is very jerky , The cell moves towards right before expanding and is not smooth at all. Unfortunately i cant post a pic/gif as a newbie to this site.

Animation when inserting a larger row into a UITableView assuming the wrong height

I'm having a problem in animating the addition or removal of a row in a UITableView which has a different height than other rows.
The following gifs demonstrats the issue with rows of the default height (44pts) and an larger row (100pts) being inserted and removed. The one on the left is a screen recording from the simulator (the new cell ending up covering row five is a different issue) and the one on the right is a mockup of what it should do.
In my case, I have a bunch of rows, each 60pts in height. When a button in the cell is tapped, an "edit" cell will slide out from underneath, pushing lower cells down. This edit cell is 180pts high. When I call insertRowsAtIndexPaths:withRowAnimation: or deleteRowsAtIndexPaths:withRowAnimation:, the animation assumes the wrong height of 60pts, instead of the 180pts it should be
This means that in the case of UITableViewRowAnimationTop the new cell appears at -60pts from the position it will end up at, and slides down to its new position; about a third of the animation it should be doing. Meanwhile, the row below animates smoothly from its starting position to 180pts downward, exactly as it should.
Has anyone worked out an actual solution to this? some way to tell the new row what hight it's supposed to be for the animation?
Below is the code I am using to hide and show the edit row. I'm using a TLSwipeForOptionsCell to trigger the edit, but it's easily replicated using for example tableView:didSelectRowAtIndexPath:
-(void)hideEditFields{
[self.tableView beginUpdates];
[self.tableView deleteRowsAtIndexPaths:#[[NSIndexPath indexPathForItem:editFormVisibleForRow+1 inSection:0]] withRowAnimation:UITableViewRowAnimationTop];
editFormVisibleForRow = -1;
[self.tableView endUpdates];
}
-(void)cellDidSelectMore:(TLSwipeForOptionsCell *)cell{
NSIndexPath* indexPath = [self.tableView indexPathForCell:cell];
// do nothing if this is the currently selected row
if(editFormVisibleForRow != indexPath.row){
if(editFormVisibleForRow >= 0){
[self hideEditFields];
// update the index path, as the cell positions (may) have changed
indexPath = [self.tableView indexPathForCell:cell];
}
[self.tableView beginUpdates];
editFormVisibleForRow = indexPath.row;
[self.tableView insertRowsAtIndexPaths:#[
[NSIndexPath indexPathForItem:editFormVisibleForRow+1 inSection:0]
] withRowAnimation:UITableViewRowAnimationTop];
[self.tableView endUpdates];
}
}
-(NSInteger) tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section{
return _dataSource.count + (editFormVisibleForRow >= 0 ? 1 : 0);
}
-(CGFloat) tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath{
int row = indexPath.row;
if(editFormVisibleForRow >= 0 && row > editFormVisibleForRow && row <= editFormVisibleForRow + 1){
return 180.0f;
}
else return 60.0;
}
Poking around a bit, it seems like this is a common issue with no clear answer. Most of the similar questions I've found here on SO are unanswered or offer workarounds specific to the asker's situation. (examples: Problem with RowAnimation, Custom UITableViewCell height yields improper animation, UITableView animation glitch when deleting and inserting cells with varying heights).
Also, instead of trying to make one triple-sized edit row, I tried making three smaller rows and animating them, but this was not suitable because they all appeared at once. I also tried animating them one after the other but the easing made it look odd, with an obvious 3-step animation occurring, instead of the whole edit view sliding out of view in one motion.
Edit: I've just noticed that if I call reloadRowsAtIndexPaths:withRowAnimation: UITableViewRowAnimationNone for the row above the one I'm trying to animate, it changes the behaviour of the animation; namely the animation assumes the height is 0pts, as demonstrated in the following animation. It's closer to what I want, but still not right, as the animation speed is wrong and it leaves a gap (in my app this means the background
colour pokes through)
The solution is pretty straight forward. You need to insert the cell with a height of 0, then change the height to the expected size and then call beginUpdates and endUpdates.
Here is some pseudo code.
var cellHeight: CGFloat = 0
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
let dynamicHeightIndex = 2
if indexPath.row == dynamicHeightIndex {
return cellHeight
} else {
return tableView.rowHeight
}
}
func insertCell() {
// First update the data source before inserting the row
tableView.insertRows(at: [someIndexPath], with: .none)
cellHeight = 200
tableView.beginUpdates()
tableView.endUpdates()
}
To remove the cell, you'll need to wait until the updates animation completes before removing from the table view.
In iOS 11 you have the func performBatchUpdates(_:completion:) which provides a completion block. For previous versions you can try using the CATransaction completion.
cellHeight = 0
CATransaction.begin()
CATransaction.setCompletionBlock({
self.tableView.deleteRows(at: [someIndexPath], with: .none)
})
tableView.beginUpdates()
tableView.endUpdates()
CATransaction.commit()
This, using didSelectRowAtIndexPath worked for me:
#interface TableController ()
#property (strong,nonatomic) NSArray *theData;
#property (strong,nonatomic) NSIndexPath *pathToEditCell;
#end
#implementation TableController
- (void)viewDidLoad {
[super viewDidLoad];
self.theData = #[#"One",#"Two",#"Three",#"Four",#"Five",#"Six",#"Seven",#"Eight",#"Nine"];
[self.tableView reloadData];
}
-(CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
return ([indexPath isEqual:self.pathToEditCell])? 100: 44;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return (self.pathToEditCell == nil)? self.theData.count: self.theData.count + 1;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
if ([indexPath isEqual:self.pathToEditCell]) {
RDEditCell *cell = [tableView dequeueReusableCellWithIdentifier:#"EditCell" forIndexPath:indexPath];
return cell;
}else{
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:#"Cell" forIndexPath:indexPath];
cell.textLabel.text = self.theData[indexPath.row];
return cell;
}
}
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath{
if (self.pathToEditCell == nil) { // first time selecting a row
self.pathToEditCell = [NSIndexPath indexPathForRow:indexPath.row +1 inSection:indexPath.section];
[self.tableView insertRowsAtIndexPaths:#[self.pathToEditCell] withRowAnimation:UITableViewRowAnimationAutomatic];
}else if ([self.pathToEditCell isEqual:indexPath]){ // deletes the edit cell if you click on it
self.pathToEditCell = nil;
[self.tableView deleteRowsAtIndexPaths:#[indexPath] withRowAnimation:UITableViewRowAnimationFade];
}else{ // close the old edit cell and adds another if you click on another cell while the edit cell is on screen
[self.tableView beginUpdates];
[self.tableView deleteRowsAtIndexPaths:#[self.pathToEditCell] withRowAnimation:UITableViewRowAnimationFade];
self.pathToEditCell = indexPath;
[self.tableView insertRowsAtIndexPaths:#[indexPath] withRowAnimation:UITableViewRowAnimationAutomatic];
[self.tableView endUpdates];
}
}
For the deletions, I like the looks of the "fade" option for the animation, but "top" also was ok.

Custom UITableViewCell height yields improper animation

I currently am working with a uitableview that holds mostly standard size cells at 44pts. However, there are a couple that are larger, about 160pts.
In this instance, there are 2 rows at 44pts height, with the larger 160pts row being inserted below, at index 2 in the section.
Removal call:
- (void)removeRowInSection:(TableViewSection *)section atIndex:(NSUInteger)index {
NSUInteger sectionIndex = [self.sections indexOfObject:section];
NSIndexPath *removalPath = [NSIndexPath indexPathForRow:index inSection:sectionIndex];
[self.tableView beginUpdates];
[self.tableView deleteRowsAtIndexPaths:#[removalPath] withRowAnimation:UITableViewRowAnimationAutomatic];
[self.tableView endUpdates];
}
Delegate call:
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
TableViewSection *section = [self sectionAtIndex:indexPath.section];
return [section heightForRowAtIndex:indexPath.row];
}
section call:
- (NSInteger)heightForRowAtIndex:(NSInteger)index {
StandardCell *cell = (StandardCell *)[self.list objectAtIndex:index];
return cell.height;
}
cell call:
- (CGFloat)height {
return 160;
}
What has me confused is when I remove the larger rows from the table, they start to animate, moving underneath the row above. But when they get to a certain point, about a 1/4 of the way through the animation, they disappear instead of finishing the animation.
It seems like the table animates the row with the notion that it's only 44pts, then once it's reached the point where 44pts are underneath the row above, it gets removed from the table. What detail have I overlooked that will give the table the correct notion to automatically animate the row removal?
Thanks for your help.
Update:
I tried commenting out the height function above (which overrides the default that returns 44). This results in a proper animation with no skips. FWIW
One way to solve this is to animate the row height down to 44 just before deleting:
//mark index paths being deleted and trigger `contentSize` update
self.indexPathsBeingDeleted = [NSMutableArray arrayWithArray:#[indexPath]];
[tableView beginUpdates];
[tableView endUpdates];
//delete row
[self.tableView deleteRowsAtIndexPaths:#[removalPath] withRowAnimation:UITableViewRowAnimationAutomatic];
[tableView endUpdates];
And then in your heightForRowAtIndexPath:
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
if ([self.indexPathsBeingDeleted containsObject:indexPath]) {
//return normal height if cell is being deleted
[self.indexPathsBeingDeleted removeObject:indexPath];
return 44;
}
if (<test for tall row>) {
return 160;
}
return 44;
}
There's a little bit of bookkeeping going on to keep track of index paths being deleted. There are probably cleaner ways to do this. This is just the first thing that came to mind. Here's a working sample project.

Resources