Search code examples
iosuitableviewanimationreloaddata

UITableView delay reloadData until animation complete


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?


Solution

  • 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.
        }
    }