11

I want to do a series of things in reaction to the end of certain UITableView animations. For example, I want the table view cell to highlight (via selectRowAtIndexPath) after it has been scrolled to via scrollToRowAtIndexPath.

How can this be done?

Boon
  • 40,656
  • 60
  • 209
  • 315

3 Answers3

31

Basic template:

[UIView animateWithDuration:0.2 animations:^{
    //do some animations, call them with animated:NO
} completion:^(BOOL finished){
    //Do something after animations finished     
}];   

Example: Scroll to row 100. When finished, get the cell at this row and make the cell content view with tag=1 to the firstResponder:

[UIView animateWithDuration:0.2 animations:^{
        [self.tableView scrollToRowAtIndexPath:[NSIndexPath indexPathForRow:100 inSection:0] atScrollPosition:UITableViewScrollPositionTop animated:NO];
    } completion:^(BOOL finished){
        //Do something after scrollToRowAtIndexPath finished, e.g.:
        UITableViewCell *nextCell = [self.tableView cellForRowAtIndexPath:[NSIndexPath indexPathForRow:100 inSection:0]];
        [[nextCell.contentView viewWithTag:1] becomeFirstResponder];        
    }];   
Przemyslaw
  • 335
  • 3
  • 3
  • This is by far the cleanest answer on SO for "How to perform X after scrollToRowAtIndexPath is complete?" Thanks! – Ryan Romanchuk Feb 26 '13 at 10:25
  • 2
    I don't know how this solution was working on iOS 4/5 but now on iOS 6.1.4 and I'm noticing that right after the contentOffset is changed by scrollToRowAtIndexPath, the cells at this position are not visible, so for a fraction of second the tableview is completely blank, then the cells are drawn and the keyboard is displayed as expected. I guess a UITableView is not meant to be used in this way, or at least not in this version of iOS. The same result happens using setContentOffset instead of scrollToRowAtIndexPath. – ggould75 May 23 '13 at 10:38
  • 4
    It am guessing it does not work in iOS 6 anymore because has changed the timing of the animations. Check the following SO answer for a more reliable method: http://stackoverflow.com/a/12516056/908621 – fishinear Jul 29 '13 at 16:19
2

I realize this an old post but I was having a similar problem and created a solution that worked well for me. I applied the techniques used on NSCookBook for creating UIAlertViews with blocks. The reason I went for this was because I wanted to use the built-in animations rather than UIView's + animateWithDuration:animations:completion:. There is a larger difference between these animations with the change to iOS 7.

You create a category for UITableView and in the implementation file you create an inner private class that will callback the block by assigning it as your tableview's delegate. The catch is that until the block is called, the original delegate will be "lost" so to speak, since the new delegate is the object that will call the block. That is why I put a notification to send a message when the block has been called to reassign the original UITableViewDelegate. This code has been tested and is working on my end.

// Header file
@interface UITableView (ScrollDelegateBlock)

-(void)scrollToRowAtIndexPath:(NSIndexPath *)indexPath
             atScrollPosition:(UITableViewScrollPosition)scrollPosition
                     animated:(BOOL)animated
               scrollFinished:(void (^)())scrollFinished;

@end


// Implementation file
#import "UITableView+ScrollDelegateBlock.h"
#import <objc/runtime.h>

NSString *const BLOCK_CALLED_NOTIFICATION = @"BlockCalled";

@interface ScrollDelegateWrapper : NSObject <UITableViewDelegate>

@property (copy) void(^scrollFinishedBlock)();

@end

@implementation ScrollDelegateWrapper

-(void)scrollViewDidEndScrollingAnimation:(UIScrollView *)scrollView {
    if (self.scrollFinishedBlock) {
        [[NSNotificationCenter defaultCenter] postNotificationName:BLOCK_CALLED_NOTIFICATION object:nil];
        self.scrollFinishedBlock();
    }
}

@end

static const char kScrollDelegateWrapper;

static id<UITableViewDelegate>previousDelegate;

@implementation UITableView (ScrollDelegateBlock)

-(void)scrollToRowAtIndexPath:(NSIndexPath *)indexPath
             atScrollPosition:(UITableViewScrollPosition)scrollPosition
                     animated:(BOOL)animated
               scrollFinished:(void (^)())scrollFinished {
    previousDelegate = self.delegate;
    ScrollDelegateWrapper *scrollDelegateWrapper = [[ScrollDelegateWrapper alloc] init];
    scrollDelegateWrapper.scrollFinishedBlock = scrollFinished;
    self.delegate = scrollDelegateWrapper;

    objc_setAssociatedObject(self, &kScrollDelegateWrapper, scrollDelegateWrapper, OBJC_ASSOCIATION_RETAIN_NONATOMIC);

    [self scrollToRowAtIndexPath:indexPath atScrollPosition:scrollPosition animated:animated];

    [[NSNotificationCenter defaultCenter] addObserver:self
                                             selector:@selector(blockCalled:)
                                                 name:BLOCK_CALLED_NOTIFICATION
                                               object:nil];
}

/*
 * Assigns delegate back to the original delegate
 */
-(void) blockCalled:(NSNotification *)notification {
    self.delegate = previousDelegate;
    [[NSNotificationCenter defaultCenter] removeObserver:self
                                                    name:BLOCK_CALLED_NOTIFICATION
                                                  object:nil];
}

@end

You can then call the method like any other with a block:

[self.tableView scrollToRowAtIndexPath:self.currentPath
                          atScrollPosition:UITableViewScrollPositionMiddle
                                  animated:YES
                            scrollFinished:^{
                                NSLog(@"scrollFinished");
                            }
];
Chris
  • 1,663
  • 1
  • 15
  • 19
  • Thank you for posting this. Converted it to my collection view and works perfect. – Augie Nov 08 '13 at 14:14
  • 2
    quick note: if the scrollview is already at top, the completion block doesn't trigger. need to check position first. – Augie Nov 08 '13 at 15:35
  • This works, however it seems to ignore the heightForCell's during/after the scroll. I have a smaller cell at the bottom of each section, however this method makes the smaller cells the same size as all the others. – Darren Jan 13 '15 at 12:29
  • Didn't work as I thought it would. The completion block was never called. – Jasper May 26 '15 at 08:31
  • @JasperPol, what are you doing different? It worked for me as well as others that have commented here. Remember that this is an alternative fix, not an end all. For instance, please see this link for another alternative http://stackoverflow.com/questions/7623771/how-to-detect-that-animation-has-ended-on-uitableview-beginupdates-endupdates/12516056#12516056 – Chris May 27 '15 at 02:51
  • @Augie You could call `self.contentOffset = CGPointMake(self.contentOffset.x, self.contentOffset.y + 0.5);` first to ensure that the scrolling always happen and completion block is always called. (0.5 is used instead of 1 to avoid visual glitch.) But in the end I gave up and pass `animated:NO` instead to completely avoid this hack. – 0xced Feb 01 '18 at 16:31
-3

Well if you want to perform an action once the scrollToRowAtIndexPath has been fired.

- (void)scrollToRowAtIndexPath:(NSIndexPath *)indexPath atScrollPosition:(UITableViewScrollPosition)scrollPosition animated:(BOOL)animated

You need to create a CAAnimation pointer like

CAAnimation *myAnimation;

Then set the delgate to self

myAnimation.delegate = self;

Once you do that, these following delegate methods should activate where you can put your code:

- (void)animationDidStart:(CAAnimation *)theAnimation

- (void)animationDidStop:(CAAnimation *)theAnimation finished:(BOOL)flag
Brock Woolf
  • 46,656
  • 50
  • 121
  • 144