UITableViewCell doesn't get deselected when swiping back quickly

IosUitableviewIos7

Ios Problem Overview


I've now updated three of my apps to iOS 7, but in all three, despite them not sharing any code, I have the problem where if the user swipes to go back in the navigation controller (rather than tap the back button) quickly, the cell will remain in its selected state.

For the three apps, one uses custom cells created programmatically, another uses custom cells created in a storyboard and the third uses default cells in a very basic subclass of UITableView, also in a storyboard. In all three cases, the cells don't deselect by themselves. If the user swipes slowly, or hits the back button, they deselect as normal.

This is only happening in my iOS 7 apps, Apple's own apps and third party apps upgraded for iOS 7 all seem to be behaving normally (albeit with slight differences in how quickly the cells gets deselected).

There must be something I'm doing wrong, but I'm not sure what?

Ios Solutions


Solution 1 - Ios

This worked best for me:

- (void)viewDidLoad {
    [super viewDidLoad];
    self.clearsSelectionOnViewWillAppear = NO;
}

-(void)viewWillAppear:(BOOL)animated
{
    [super viewWillAppear:animated];
    [self.tableView deselectRowAtIndexPath:[self.tableView indexPathForSelectedRow] animated:animated];
}

I even got a much better unselect fading while I was swiping back slowly.

Solution 2 - Ios

I'm dealing with the same problem right now. The UICatalog-sample from Apple seems to bring the dirty solution.

It really doesn't make me happy at all. As mentioned before it uses [self.tableView deselectRowAtIndexPath:tableSelection animated:NO]; to deselect the currently selected row.

- (void)viewWillAppear:(BOOL)animated
{
	[super viewWillAppear:animated];
    
    // this UIViewController is about to re-appear, make sure we remove the current selection in our table view
	NSIndexPath *tableSelection = [self.tableView indexPathForSelectedRow];
	[self.tableView deselectRowAtIndexPath:tableSelection animated:NO];
    
    // some over view controller could have changed our nav bar tint color, so reset it here
    self.navigationController.navigationBar.tintColor = [UIColor darkGrayColor];
}

I have to mention the sample code may not be iOS 7 iOS 8 iOS 9 iOS 10-ready


Something which really confuses me is the UITableViewController Class Reference:

> When the table view is about to appear the first time it’s loaded, the > table-view controller reloads the table view’s data. It also clears > its selection (with or without animation, depending on the request) > every time the table view is displayed. The UITableViewController > class implements this in the superclass method viewWillAppear:. You > can disable this behavior by changing the value in the > clearsSelectionOnViewWillAppear property.

This is exactly the behavior I expect… but it does not seem to work. Neither for you nor for me. We really have to use the "dirty" solution and do it on our own.

Solution 3 - Ios

Fabio's answer works well but doesn't give the right look if the user swipes just a little bit and then changes their mind. In order to get that case right you need to save the selected index path and reset it when necessary.

- (void)viewDidAppear:(BOOL)animated
{
    [super viewDidAppear:animated];
    self.savedSelectedIndexPath = nil;
}

- (void)viewWillDisappear:(BOOL)animated
{
    [super viewWillDisappear:animated];
    if (self.savedSelectedIndexPath) {
        [self.tableView selectRowAtIndexPath:self.savedSelectedIndexPath animated:NO scrollPosition:UITableViewScrollPositionNone];
    }
}

- (void)viewWillAppear:(BOOL)animated
{
    [super viewWillAppear:animated];
    self.savedSelectedIndexPath = self.tableView.indexPathForSelectedRow;

    if (self.savedSelectedIndexPath) {
        [self.tableView deselectRowAtIndexPath:self.savedSelectedIndexPath animated:YES];
    }
}

If using a UITableViewController, make sure to disable the built-in clearing:

self.clearsSelectionOnViewWillAppear = NO;

and add the property for savedSelectedIndexPath:

@property(strong, nonatomic) NSIndexPath *savedSelectedIndexPath;

If you need to do this in a few different classes it might make sense to split it out in a helper, for example like I did in this gist: https://gist.github.com/rhult/46ee6c4e8a862a8e66d4

Solution 4 - Ios

This solution animates the row deselection along with the transition coordinator (for a user-driven VC dismiss) and re-applies the selection if the user cancels the transition. Adapted from a solution by Caleb Davenport in Swift. Only tested on iOS 9. Tested as working with both user driven (swipe) transition and the old-style "Back" button tap.

In the UITableViewController subclass:

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

    // Workaround. clearsSelectionOnViewWillAppear is unreliable for user-driven (swipe) VC dismiss
    NSIndexPath *indexPath = self.tableView.indexPathForSelectedRow;
    if (indexPath && self.transitionCoordinator) {
        [self.transitionCoordinator animateAlongsideTransition:^(id<UIViewControllerTransitionCoordinatorContext>  _Nonnull context) {
            [self.tableView deselectRowAtIndexPath:indexPath animated:animated];
        } completion:^(id<UIViewControllerTransitionCoordinatorContext>  _Nonnull context) {
            if ([context isCancelled]) {
                [self.tableView selectRowAtIndexPath:indexPath animated:NO scrollPosition:UITableViewScrollPositionNone];
            }
        }];
    }
}

Solution 5 - Ios

After running into this myself today I found out that this apparently is a fairly well-known problem with UITableView, its support for interactive navigation transitions is slightly broken. The folks behind Castro have posted an excellent analysis and solution to this: http://blog.supertop.co/post/80781694515/viewmightappear

I decided to use their solution which also considers cancelled transitions:

- (void)viewWillAppear:(BOOL)animated
{
    [super viewWillAppear:animated];
    
    NSIndexPath *selectedRowIndexPath = [self.tableView indexPathForSelectedRow];
    if (selectedRowIndexPath) {
        [self.tableView deselectRowAtIndexPath:selectedRowIndexPath animated:YES];
        [[self transitionCoordinator] notifyWhenInteractionEndsUsingBlock:^(id<UIViewControllerTransitionCoordinatorContext> context) {
            if ([context isCancelled]) {
                [self.tableView selectRowAtIndexPath:selectedRowIndexPath animated:NO scrollPosition:UITableViewScrollPositionNone];
            }
        }];
    }
}

Solution 6 - Ios

You can try to set

self.clearsSelectionOnViewWillAppear = YES;

in a UITableViewController or

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

in viewWillAppear, maybe before calling [super viewWillAppear:animated]; If your UItableView is not inside an UITableViewController you must deselect the cells manually:

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
    [tableView deselectRowAtIndexPath:indexPath animated:YES]; 
}

Solution 7 - Ios

Simple Swift 3/4 Answer:

override func viewWillAppear(_ animated: Bool) {
    if tableView.indexPathForSelectedRow != nil {
        self.tableView.deselectRow(at: tableView.indexPathForSelectedRow! as IndexPath, animated: true)
    }
}

Solution 8 - Ios

I'm using

[tableView deselectRowAtIndexPath:indexPath animated:YES];

at the end of method

(void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath

Like this:

(void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath

{
 
    //doing something according to selected cell...
    
    [tableView deselectRowAtIndexPath:indexPath animated:YES];
}

Solution 9 - Ios

Use

[tableView deselectRowAtIndexPath:indexPath animated:YES];

code in

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath method

Solution 10 - Ios

I've found a very simple solution to this problem that just makes the default behavior work as it should. I wasn't satisfied with the solutions involving deselectRowAtIndexPath since the resulting visual effect was slightly different.

All you have to do in order to prevent this weird behavior is to reload the table when the view is displayed:

- (void)viewDidAppear:(BOOL)animated {
    [super viewDidAppear:animated];
    [self.tableView reloadData];
}

Solution 11 - Ios

Codestage answer, in Swift 3. notifyWhenInteractionEnds is deprecated.

override func viewWillAppear(_ animated: Bool) {
    super.viewWillAppear(true)
    
    if let indexPath = self.tableView.indexPathForSelectedRow {
        self.tableView.deselectRow(at: indexPath, animated: true)
        self.transitionCoordinator?.notifyWhenInteractionChanges { (context) in
            if context.isCancelled {
                self.tableView.selectRow(at: indexPath, animated: false, scrollPosition: .none)
            }
        }
    }
}

Solution 12 - Ios

You are probably not calling the super view's viewWillAppear method ([super viewWillAppear:animated];). When you do this and the UITableViewController's parameter clearsSelectionOnViewWillAppear is YES then the cells will be deselected on viewWillAppear.

Solution 13 - Ios

Based on Rhult's code, I made a few changes.

This implementation allow user to cancel swipe back and still keep selected for future swipe back deselect animation

@property(strong, nonatomic) NSIndexPath *savedSelectedIndexPath;


- (void)viewDidLoad {
   [super viewDidLoad];
   self.clearsSelectionOnViewWillAppear = NO;
}

-(void) viewDidAppear:(BOOL)animated {
   [super viewDidAppear:animated];
   self.savedSelectedIndexPath = nil;
}

-(void) viewWillDisappear:(BOOL)animated {
   [super viewWillDisappear:animated];
   if (self.savedSelectedIndexPath && ![self.tableView indexPathForSelectedRow]) {
       [self.tableView selectRowAtIndexPath:self.savedSelectedIndexPath animated:NO scrollPosition:UITableViewScrollPositionNone];
   } else {
      self.savedSelectedIndexPath = [self.tableView indexPathForSelectedRow];
   }
}

Solution 14 - Ios

Rhult's solution works perfectly on iOS 9.2. This is the implementation in Swift:

Declare a variable in your MasterViewController to save the IndexPath:

var savedSelectedIndexPath: NSIndexPath?

Then you can put the code in an extension for clarity:

extension MasterViewController {
    override func viewDidAppear(animated: Bool) {
        super.viewDidAppear(animated)
        self.savedSelectedIndexPath = nil
    }
    
    override func viewWillDisappear(animated: Bool) {
        super.viewWillDisappear(animated)
        if let indexPath = self.savedSelectedIndexPath {
            self.tableView.selectRowAtIndexPath(indexPath, animated: false, scrollPosition: .None)
        }
    }
    
    override func viewWillAppear(animated: Bool) {
        super.viewWillAppear(animated)
        self.savedSelectedIndexPath = tableView.indexPathForSelectedRow
        if let indexPath = self.savedSelectedIndexPath {
            self.tableView.deselectRowAtIndexPath(indexPath, animated: true)
        }
    }
}

Solution 15 - Ios

Codestage provided by far the best looking answer, so I decided to convert it into Swift 2.

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

        let selectedRowIndexPath = self.tableView.indexPathForSelectedRow
        if ((selectedRowIndexPath) != nil) {
            self.tableView.deselectRowAtIndexPath(selectedRowIndexPath!, animated: true)
            self.transitionCoordinator()?.notifyWhenInteractionEndsUsingBlock({ context in
                if (context.isCancelled()) {
                    self.tableView.selectRowAtIndexPath(selectedRowIndexPath, animated: false, scrollPosition: UITableViewScrollPosition.None)
                }
            })
        }
    }

Solution 16 - Ios

For swift

override func viewWillAppear(animated: Bool) {
    super.viewWillAppear(animated)
    
    guard let indexPath = tableView.indexPathForSelectedRow else{
        return
    }
    tableView.deselectRowAtIndexPath(indexPath, animated: true)
}

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionRobertView Question on Stackoverflow
Solution 1 - IosemrebergeView Answer on Stackoverflow
Solution 2 - IosFabio PoloniView Answer on Stackoverflow
Solution 3 - IosRhultView Answer on Stackoverflow
Solution 4 - Iosnevan kingView Answer on Stackoverflow
Solution 5 - IosCodeStageView Answer on Stackoverflow
Solution 6 - IosfalsecryptView Answer on Stackoverflow
Solution 7 - IosSteffenKView Answer on Stackoverflow
Solution 8 - IosEvgenyView Answer on Stackoverflow
Solution 9 - IosPradhyuman sinhView Answer on Stackoverflow
Solution 10 - IosquentezView Answer on Stackoverflow
Solution 11 - IosMikeView Answer on Stackoverflow
Solution 12 - IosBessiView Answer on Stackoverflow
Solution 13 - IosSimonView Answer on Stackoverflow
Solution 14 - IosKymerView Answer on Stackoverflow
Solution 15 - IosAppreciateItView Answer on Stackoverflow
Solution 16 - IosSofedaView Answer on Stackoverflow