Jordan H
Jordan H

Reputation: 55705

Interactively deselect selected cell with swipe back gesture

In apps such as Settings, when you tap on a cell which pushes a screen, then you swipe back from the left of the screen, you can see the deselection of the selected cell’s background color fading, and it's fully interactive - if you swipe half way then swipe back the selected background view returns to full opacity.

In my app, I haven't changed any of the default behavior, and when I swipe from the left to go back, the selected cell background color remains completely opaque until the swipe gesture is completed, and then it quickly fades to deselect it.

How can one implement the interactive deselection of cells via the swipe to go back gesture?

Upvotes: 14

Views: 1924

Answers (3)

Jinwoo Kim
Jinwoo Kim

Reputation: 607

Using Private API (unsafe - Apple will reject your app)

#import <objc/message.h>
#import <objc/runtime.h>

- (void)viewWillAppear:(BOOL)animated {
    [super viewWillAppear:animated];
    
    ((void (*)(id, SEL, BOOL, id))objc_msgSend)(self.collectionView, sel_registerName("_deselectAllAnimated:notifyDelegate:"), YES, nil);
}

Upvotes: 0

Jordan H
Jordan H

Reputation: 55705

To enable interactive deselection in iOS 11 and newer, you can use UITableViewController because it implements it for you, or you can implement it by animating the deselection alongside the transition coordinator, like so:

- (void)viewWillAppear:(BOOL)animated { 
    [super viewWillAppear:animated]; 

    NSIndexPath *selectedIndexPath = [self.tableView indexPathForSelectedRow];
    if (selectedIndexPath) {
        if (self.transitionCoordinator) { 
            [self.transitionCoordinator 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]; 
        }
    }
}

And in Swift:

override func viewWillAppear(_ animated: Bool) {
    super.viewWillAppear(animated)

    if let selectedIndexPath = tableView.indexPathForSelectedRow {
        if let coordinator = transitionCoordinator {
            coordinator.animate(alongsideTransition: { context in
                self.tableView.deselectRow(at: selectedIndexPath, animated: true)
            }) { context in
                if context.isCancelled {
                    self.tableView.selectRow(at: selectedIndexPath, animated: false, scrollPosition: .none)
                }
            }
        } else {
            self.tableView.deselectRow(at: selectedIndexPath, animated: animated)
        }
    }
}

A similar implementation works with UICollectionView:

override func viewWillAppear(_ animated: Bool) {
    super.viewWillAppear(animated)

    if let indexPath = collectionView.indexPathsForSelectedItems?.first {
        if let coordinator = transitionCoordinator {
            coordinator.animate(alongsideTransition: { _ in
                self.collectionView.deselectItem(at: indexPath, animated: true)
            }, completion: { context in
                if context.isCancelled {
                    self.collectionView.selectItem(at: indexPath, animated: false, scrollPosition: [])
                }
            })
        } else {
            collectionView.deselectItem(at: indexPath, animated: animated)
        }
    }
}

Upvotes: 22

Dima
Dima

Reputation: 23624

It seems as though the clearsSelectionOnViewWillAppear might actually be getting called by viewDidAppear: rather than viewWillAppear: The change only happens once the transition is completely over and if you cancel the interactive transition it does not happen at all (if it was in viewWillAppear:, it would). This looks like a UIKit bug as the docs clearly state it should be getting called in viewWillAppear:

Put the following line of code into viewWillAppear: and you will get the exact behavior you are looking for, I just tried it. This is probably the exact behavior that property triggers, just in the wrong method.

[self.tableView deselectRowAtIndexPath:[self.tableView indexPathForSelectedRow] animated:YES];

Upvotes: 13

Related Questions