Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
Animate table view deselection alongside interactive transition on iOS 11
/*
In iOS 11, interactive view controller transitions no longer scrub by setting the layer speed to zero
and changing the timeOffset. As a result of this change, implicit animations that occur in places like
-viewWillAppear: (called during an interactive transition) no longer end up “caught in” the animation.
To get the same behavior for table view row deselection as before, you can either use UITableViewController
which implements this for you, or you can implement it manually by deselecting the row in an alongside
animation for the transition (set up in -viewWillAppear: using the transition coordinator).
Here is an example implementation which correctly handles some of the more subtle corner cases:
*/
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
NSIndexPath *selectedIndexPath = [self.tableView indexPathForSelectedRow];
if (selectedIndexPath != nil) {
id<UIViewControllerTransitionCoordinator> coordinator = self.transitionCoordinator;
if (coordinator != nil) {
[coordinator animateAlongsideTransition:^(id<UIViewControllerTransitionCoordinatorContext> context) {
[self.tableView deselectRowAtIndexPath:selectedIndexPath animated:YES];
} completion:^(id<UIViewControllerTransitionCoordinatorContext> context) {
if (context.cancelled) {
[self.tableView selectRowAtIndexPath:selectedIndexPath animated:NO scrollPosition:UITableViewScrollPositionNone];
}
}];
} else {
[self.tableView deselectRowAtIndexPath:selectedIndexPath animated:animated];
}
}
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
if let selectedIndexPath = tableView.indexPathForSelectedRow {
if let coordinator = transitionCoordinator {
coordinator.animate(alongsideTransition: { _ in
self.tableView.deselectRow(at: selectedIndexPath, animated: true)
}, completion: { context in
if context.isCancelled {
self.tableView.selectRow(at: selectedIndexPath, animated: false, scrollPosition: .none)
}
})
} else {
tableView.deselectRow(at: selectedIndexPath, animated: animated)
}
}
}
@ogres

This comment has been minimized.

Copy link

commented Dec 21, 2017

Thanks for the nice tip!

How about putting it into an extension like:

extension UITableView {
    
    func deselectRow(along transitionCoordinator: UIViewControllerTransitionCoordinator?) {
        guard let selectedIndexPath = indexPathForSelectedRow else { return }
        
        guard let coordinator = transitionCoordinator else {
            deselectRow(at: selectedIndexPath, animated: false)
            return
        }
        
        coordinator.animate(alongsideTransition: { _ in
            self.deselectRow(at: selectedIndexPath, animated: true)
        }) { (context) in
            if context.isCancelled {
                self.selectRow(at: selectedIndexPath, animated: false, scrollPosition: .none)
            }
        }
    }
}

and then just call tableView.deselectRow(along: transitionCoordinator) from viewWillAppear ? It will be easier to reuse this functionality

@saagarjha

This comment has been minimized.

Copy link

commented Dec 22, 2017

Thanks for putting this up, @smileyborg! I was trying to figure out how UITableViewController was doing it by disassembling -[UITableViewController viewWillAppear:] and trying to emulate it, but couldn't quite get it right. Quick question: do we have to deal with checking initiallyInteractive and isInterruptible as UIKit does?

@xmollv

This comment has been minimized.

Copy link

commented Feb 17, 2018

Thanks for sharing! I've moved the code to an extension on UITableView to be able to reuse it easily on any UIViewController (as @ogres suggested). Here it is in case that anyone needs it:

extension UITableView {
    func deselectRow(with transitionCoordinator: UIViewControllerTransitionCoordinator?, animated: Bool) {
        guard let selectedIndexPath = self.indexPathForSelectedRow else { return }
        guard let coordinator = transitionCoordinator else {
            self.deselectRow(at: selectedIndexPath, animated: animated)
            return
        }
        
        coordinator.animate(alongsideTransition: { _ in
            self.deselectRow(at: selectedIndexPath, animated: true)
        }, completion: { context in
            if context.isCancelled {
                self.selectRow(at: selectedIndexPath, animated: false, scrollPosition: .none)
            }
        })
    }
}

And the usage from any UIViewController:

override func viewWillAppear(_ animated: Bool) {
    super.viewWillAppear(animated)
    self.tableView.deselectRow(with: self.transitionCoordinator, animated: animated)
}

Cheers!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.