I've got a UITableView that represents a checklist of items. Each item is in a done/undone state.
I'd like to keep all of the done items at the top of the list, so when the user clicks an item that is undone, I'd like to figure out where the row should go (at end of currently-done items list) and move it there.
I'm trying to use -moveRowAtIndexPath:toIndexPath:
for my UITableView to do this.
It works well sometimes and not so well at other times.
It seems to work well when the done action kicks off another animation elsewhere on screen. For some reason, this seems to serve as a delay for the -reloadData
call.
When that's not true (i.e., the only thing "happening" is the row being marked done and moving), the animation seems to get short-circuited by an automatic call to the UITableView's -reloadData
method. That is, the animation begins, but about halfway through, -reloadData
is called and the rows snap to their final position. It's fairly jarring from the user's perspective.
I've tracked through my code to verify that I'm not calling -reloadData
myself, and it doesn't appear that I'm the one triggering this -reloadData
call.
I'm OK with the automatic call to -reloadData
and I understand why it's called (though you'd think it might not be necessary, but that's a different issue), but I'd really like it to wait until it completes its animation.
Here's the code I'm using:
NSIndexPath *oldPath = [NSIndexPath indexPathForRow:currentIndex inSection:0];
NSIndexPath *newPath = [NSIndexPath indexPathForRow:newIndex inSection:0];
[tableView beginUpdates];
[checklist removeObject:task];
[checklist insertObject:task atIndex:newIndex];
[tableView moveRowAtIndexPath:oldPath toIndexPath:newPath];
[tableView endUpdates];
Am I screwing something up?
At the request of titaniumdecoy, here's how I got this issue fixed:
I have a UIViewController
subclass with an NSTimer
that executes once a second, checking the underlying data source for a UITableView
for changes that indicate a call to -reloadData
is necessary. So, to that subclass I added:
@property (BOOL) IsAnimating;
I set this initially to NO
and if the isAnimating
property is set to YES
, the NSTimer
"short-circuits" and skips its normal processing.
So, when I want to run this UITableView
animation, I set the isAnimating
property to YES
and run the animation.
Then, I schedule a selector to run 1 second in the future that will reset isAnimating
to NO
. The NSTimer
will then continue firing and will see an isAnimating
of NO
(most likely on the second subsequent call to -codeTimerFired
) and then find the data source update, kicking off a call to reloadData
.
Here's the code for suspending the NSTimer
's processing and scheduling the UITableView
animation:
// currentIndex and newIndex have already been calculated as the data item's
// original and destination indices (only 1 section in UITableView)
if (currentIndex != newIndex) {
// This item's index has changed, so animate its movement
NSIndexPath *oldPath = [NSIndexPath indexPathForRow:currentIndex inSection:0];
NSIndexPath *newPath = [NSIndexPath indexPathForRow:newIndex inSection:0];
// Set a flag to disable data source processing and calls to [UITableView reloadData]
[[self Owner] setIsAnimating:YES];
// I haven't tested enough, but some documentation leads me to believe
// that this particular call (-moveRowAtIndexPath:toIndexPath:) may NOT need
// to be wrapped in a -beginUpdates/-endUpdates block
[[[self Owner] InstructionsTableView] beginUpdates];
// These lines move the item in my data source
[[[MMAppDelegate singleton] CurrentChecklist] removeObject:[self CellTask]];
[[[MMAppDelegate singleton] CurrentChecklist] insertObject:[self CellTask] atIndex:newIndex];
// This code is the UITableView animation
[[[self Owner] InstructionsTableView] moveRowAtIndexPath:oldPath toIndexPath:newPath];
// Conclude the UITableView animation block
[[[self Owner] InstructionsTableView] endUpdates];
// Schedule a call to re-enable UITableView animation
[[self Owner] performSelector:@selector(setIsAnimating:) withObject:@(NO) afterDelay:1.0];
} else {
// This location hasn't changed, so just tell my owner to reload its data
[[[self Owner] InstructionsTableView] reloadData];
}
Here's the NSTimer
method (note how it bails out if isAnimating == YES
):
- (void)codeTimerFired {
// This is a subclass of a template subclass...
// super actually has work to do in this method...
[super codeTimerFired];
// If we're in the middle of an animation, don't update!
if ([self IsAnimating]) {
return;
}
// Other data source processing...
// local BOOL to check whether underlying data source has changed
BOOL shouldUpdate = NO;
// code to check if underlying data source has changed...
// ******************************************************
// [CODE REMOVED]
// ******************************************************
// If the underlying data source has changed, update the UITableView
if (shouldUpdate) {
[self reloadTableView]; // <--- This is the main line I wanted to prevent
// since the code that fired to cause the
// UITableView animation will ALWAYS cause
// the underlying data source to change such
// that this line would fire.
}
}