How to get notified when scrollToRowAtIndexPath fi

2020-01-27 06:24发布

问题:

This is a follow-up to How to get notified when a tableViewController finishes animating the push onto a nav stack.

In a tableView I want to deselect a row with animation, but only after the tableView has finished animating the scroll to the selected row. How can I be notified when that happens, or what method gets called the moment that finishes.

This is the order of things:

  1. Push view controller
  2. In viewWillAppear I select a certain row.
  3. In viewDidAppear I scrollToRowAtIndexPath (to the selected row).
  4. Then when that finishes scrolling I want to deselectRowAtIndexPath: animated:YES

This way, the user will know why they were scrolled there, but then I can fade away the selection.
Step 4 is the part I haven't figured out yet. If I call it in viewDidAppear then by the time the tableView scrolls there, the row has been deselected already which is no good.

回答1:

You can use the table view delegate's scrollViewDidEndScrollingAnimation: method. This is because a UITableView is a subclass of UIScrollView and UITableViewDelegate conforms to UIScrollViewDelegate. In other words, a table view is a scroll view, and a table view delegate is also a scroll view delegate.

So, create a scrollViewDidEndScrollingAnimation: method in your table view delegate and deselect the cell in that method. See the reference documentation for UIScrollViewDelegate for information on the scrollViewDidEndScrollingAnimation: method.



回答2:

try this

[UIView animateWithDuration:0.3 animations:^{
    [yourTableView scrollToRowAtIndexPath:indexPath 
                         atScrollPosition:UITableViewScrollPositionTop 
                                 animated:NO];
} completion:^(BOOL finished){
    //do something
}];

Don't forget to set animated to NO, the animation of scrollToRow will be overridden by UIView animateWithDuration.

Hope this help !



回答3:

To address Ben Packard's comment on the accepted answer, you can do this. Test if the tableView can scroll to the new position. If not, execute your method immediately. If it can scroll, wait until the scrolling is finished to execute your method.

- (void)someMethod
{
    CGFloat originalOffset = self.tableView.contentOffset.y;
    [self.tableView scrollToRowAtIndexPath:path atScrollPosition:UITableViewScrollPositionMiddle animated:NO];
    CGFloat offset = self.tableView.contentOffset.y;

    if (originalOffset == offset)
    {
        // scroll animation not required because it's already scrolled exactly there
        [self doThingAfterAnimation];
    }
    else
    {
        // We know it will scroll to a new position
        // Return to originalOffset. animated:NO is important
        [self.tableView setContentOffset:CGPointMake(0, originalOffset) animated:NO];
        // Do the scroll with animation so `scrollViewDidEndScrollingAnimation:` will execute
        [self.tableView scrollToRowAtIndexPath:path atScrollPosition:UITableViewScrollPositionMiddle animated:YES];
    }
}

- (void)scrollViewDidEndScrollingAnimation:(UIScrollView *)scrollView
{
    [self doThingAfterAnimation];
}


回答4:

You can include the scrollToRowAtIndexPath: inside a [UIView animateWithDuration:...] block which will trigger the completion block after all included animations conclude. So, something like this:

[UIView
    animateWithDuration:0.3f
    delay:0.0f
    options:UIViewAnimationOptionAllowUserInteraction
    animations:^
    {
        // Scroll to row with animation
        [self.tableView scrollToRowAtIndexPath:indexPath
                            atScrollPosition:UITableViewScrollPositionTop
                                    animated:YES];
    }
    completion:^(BOOL finished)
    {
        // Deselect row
        [self.tableView deselectRowAtIndexPath:indexPath animated:YES];
    }];


回答5:

Implementing this in a Swift extension.

//strong ref required
private var lastDelegate : UITableViewScrollCompletionDelegate! = nil

private class UITableViewScrollCompletionDelegate : NSObject, UITableViewDelegate {
    let completion: () -> ()
    let oldDelegate : UITableViewDelegate?
    let targetOffset: CGPoint
    @objc private func scrollViewDidEndScrollingAnimation(scrollView: UIScrollView) {
        scrollView.delegate = oldDelegate
        completion()
        lastDelegate = nil
    }

    init(completion: () -> (), oldDelegate: UITableViewDelegate?, targetOffset: CGPoint) {
        self.completion = completion
        self.oldDelegate = oldDelegate
        self.targetOffset = targetOffset
        super.init()
        lastDelegate = self
    }
}

extension UITableView {
    func scrollToRowAtIndexPath(indexPath: NSIndexPath, atScrollPosition scrollPosition: UITableViewScrollPosition, animated: Bool, completion: () -> ()) {
        assert(lastDelegate == nil, "You're already scrolling.  Wait for the last completion before doing another one.")
        let originalOffset = self.contentOffset
        self.scrollToRowAtIndexPath(indexPath, atScrollPosition: scrollPosition, animated: false)

        if originalOffset.y == self.contentOffset.y { //already at the right position
            completion()
            return
        }
        else {
            let targetOffset = self.contentOffset
            self.setContentOffset(originalOffset, animated: false)
            self.delegate = UITableViewScrollCompletionDelegate(completion: completion, oldDelegate: self.delegate, targetOffset:targetOffset)
            self.scrollToRowAtIndexPath(indexPath, atScrollPosition: scrollPosition, animated: true)
        }
    }
}

This works for most cases although the TableView delegate is changed during the scroll, which may be undesired in some cases.