views:

740

answers:

4

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?

A: 

Seems like something somewhere is requesting element 6 from an array that only has elements at indexes 0-5 (meaning 6 elements).

This usually happens when the code tries to do:

NSUInteger index = [somearray count];
id obj = [somearray objectAtIndex:index];

because count is upper boundary and arrays start from 0 the last element is at count - 1.

This might not be directly in your code but you may be restricting something to a number of elements and then requesting one past the last element.

stefanB
Note that one should use `NSUInteger` for the result of `count`, lest it be truncated (particularly when moving code to the Mac, where `NSUInteger` is, increasingly often, larger than an `int`). It's also important to keep it unsigned so that comparisons to this never-negative value always make sense.
Peter Hosey
True, I was just being lazy ...
stefanB
I am aware of the base problem, but that code that's accessing the array is deep within the framework. Like I said, there's absolutely no code of my own on the stack at the time of the crash. At its base, it seems like a bug in the UITableView object.
Ed Marty
+1  A: 

I had a similar error with deleting that I couldn't figure out for a while -- but I put [tableView beginUpdates] and [tableView endUpdates] around the code and it fixed everything. Could be that your datasource just isn't updating before it attempts to redraw, and those methods should prevent that (worth a shot, anyway).

Ian Henry
Already done, actually.
Ed Marty
A: 

Are you updating your data model for the table in targetIndexPathForMoveFromRowAtIndexPath

Or in the DataSource delegate method: tableView:moveRowAtIndexPath:toIndexPath: ?

Taken from the Table View Programming Guide for iPhone OS, under reordering table cells:

The table view sends tableView:moveRowAtIndexPath:toIndexPath: to its data source (if it implements the method). In this method the data source updates the data-model array that is the source of items for the table view, moving the item to a different location in the array.

And for the delegate method, it is written:

Every time the dragged row is over a destination, the table view sends tableView:targetIndexPathForMoveFromRowAtIndexPath:toProposedIndexPath: to its delegate (if it implements the method). In this method the delegate may reject the current destination for the dragged row and specify an alternative one.

tableView:targetIndexPathForMoveFromRowAtIndexPath:toProposedIndexPath: is for determining whether a relocation is allowed, but actual changes to your data model should take place in tableView:moveRowAtIndexPath:toIndexPath:

Perhaps this is what you're doing, but I can't tell just from the info you provided.

wkw
No, I'm updating it in the moveRowAtIndexPath method
Ed Marty
A: 

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.

Tom Saxton
I'm glad at least one other person has had the same experience as me!
Ed Marty