Ios – Crashing while trying to move UITableView rows

cocoa-touchiosiphoneuitableview

I've got some rather complicated rules for moving rows around in a UITableView. There are an undefined number of sections and rows per section, and based on various rules, rows can be moved within or between sections by the user to specific other locations.

All of the data updating and everything is working. But occasionally, after moving a row, the app will wig out and suddenly there will be an empty space where a row should be displayed.

I'm using:

              - (NSIndexPath *)tableView:(UITableView *)tableView
targetIndexPathForMoveFromRowAtIndexPath:(NSIndexPath *)sourceIndexPath

to specify where the user is allowed to drag the rows based on where the cell is. 98% of the time it works. But in some cases, when the user is only allowed to drag between sections (can't reorder rows within the section) this error appears, then the app crashes after scrolling over the area with no row.

The exception thrown is pretty useless:

Terminating app due to uncaught exception 'NSRangeException', reason: '*** -[NSCFArray objectAtIndex:]: index (6) beyond bounds (6)

None of my code is on the stack. The last UITableView-specific method is

-[UITableView(UITableViewInternal) _visibleCellForGlobalRow:]

Has anybody seen this issue occur before? Any ideas?

Best Answer

I just hit what I believe is the same problem in my app.

The situation is that I have two table sections. Items can be dragged within and between sections. Users can drag cells to any row in the first section, but in the second section the items are sorted, so for any given cell, there's only one valid row.

If I scroll the view so that the bottom of section 1 and the top of section 2 are visible, grab an item in section 1 that sorts to the bottom of section 2, and drag it into the the top of section 2, my tableView:targetIndexPathForMoveFromRowAtIndexPath:toProposedIndexPath: method gets called and I return the correct destination position, which is several rows below the bottom of the screen. In the UI, you can see an empty cell gets created at the bottom of the screen, which is not the correct destination row.

When you let go of the cell, that bogus cell that was created at the bottom of the screen (in the middle of section 2) stays there! tableView:cellForRowAtIndexPath: never even gets called for it. As soon as you try to do anything with that cell, you crash.

My first solution was to just call [tableView reloadData] at the end of tableView:moveRowAtIndexPath:toIndexPath:. But that causes a crash, so instead I call it indirectly after a delay. But then there's another bug: after the delayed reloadData call, tableView:moveRowAtIndexPath:toIndexPath: gets called again with a bogus request to move an item one past the end of the first section to that same position. So, I had to add code to ignore bogus no-op requests.

So, here's the code:

- (void)tableView:(UITableView *)tableView moveRowAtIndexPath:(NSIndexPath *)pathSrc toIndexPath:(NSIndexPath *)pathDst
{
  // APPLE_BUG: after doing the delayed table reload (see below), we get a bogus
  // request to move a nonexistant cell to its current location
  if (pathSrc.row == pathDst.row && pathSrc.section == pathDst.section)
    return;

  // update your data model to reflect the move...

  // APPLE_BUG: if you move a cell to a row that's off-screen (because the destination
  // has been modified), the bogus cell gets created and eventually will cause a crash
  [self performSelector:@selector(delayedReloadData:) withObject:tableView afterDelay:0];
}

- (void)delayedReloadData:(UITableView *)tableView
{
  Assert(tableView == self.tableView);
  [tableView reloadData];
}

Note that there's still a UI bug. On the screen, the dragged cell gets animated into the bogus empty cell. At the end of the animation, the empty cell gets redrawn with the correct data for that row, but the observant user will notice the dragged cell getting animated to the wrong spot then instantly morphed to a different cell.

This is definitely a goofy UI. I considered scrolling the proper destination row onto the screen, but if I were to do that it would fill the screen with section two and then any attempt to drag back to section one would be continually thwarted by my (now annoying) autoscrolling. I may have to change the UI, but that would require some complex and bothersome changes to my data model.