iOS UITableViewCell setSelected:animated: always has animated = NO

6.3k views Asked by At

I'm trying to make my own selection animation. I've created a subclass of UITableViewCell. I do my selection animation in -setSelected:animated: method. It works as intended when you select or deselect cells by tapping them. Problem is that animation is also seen during scrolling, since -setSelected:animated: is called on each cell before it appears. This is how reusing cells mechanism works, I get it. What I don't get is that it always calls this method with animated = NO either on tap or on scroll. This seems like a logic mistake to me. I presumed it was supposed to select cells with animation when you tap them and without animation when reused cell appears. Is animated parameter even ever used anywhere except manual calls? Here's my code:

- (void)setSelected:(BOOL)selected animated:(BOOL)animated {

    BOOL alreadySelected = (self.isSelected) && (selected);
    BOOL alreadyDeselected = (!self.isSelected) && (!selected);

    [super setSelected:selected animated:animated];

    if ((alreadySelected) || (alreadyDeselected)) return;

    NSLog(@"Animated selection: %@", animated ? @"YES" : @"NO");

    NSTimeInterval duration;

    if (animated) {

        duration = 0.25;

    } else {

        duration = 0.0;

    }

    [CATransaction begin];
    [CATransaction setAnimationDuration:duration];

    if (selected) {

        //layer properties are changed here...


    } else {

        //layer properties are changed here... 

    }


    [CATransaction commit];


}

This always goes without the animation. I can't think of any other such an easy way to handle custom selection. Implementing -didSelectRow methods in controller seems so much worse and it's not called during scrolling, so reused cells will appear in a wrong state. Any idea how to fix this?

UPDATE:

I've found a temporary solution:

#pragma mark - Table View Delegate 

- (NSIndexPath *)tableView:(UITableView *)tableView willSelectRowAtIndexPath:(NSIndexPath *)indexPath {

    UITableViewCell *cell = [tableView cellForRowAtIndexPath:indexPath];
    [cell setSelected:YES animated:YES];   
    return indexPath; 
}


- (NSIndexPath *)tableView:(UITableView *)tableView willDeselectRowAtIndexPath:(NSIndexPath *)indexPath {

    UITableViewCell *cell = [tableView cellForRowAtIndexPath:indexPath];
    [cell setSelected:NO animated:YES];

    return indexPath;      
}

It does work, but I don't like it. The fact that TableView's delegate has to know something about selection and it's not all contained in one place bugs me a lot. And -setSelected is called twice when taping a row - with and without animation.

3

There are 3 answers

5
Dave Batton On

This is how table views were designed to work. When you select it it highlights immediately, so no animation is needed. However, you will (you should, anyway) see the table view cell deselect with animation when you pop back to the table view. You can see that by using this code in the -viewWillAppear:override:

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

That will happen for you automatically if you are using a UITableViewController and have its clearsSelectionOnViewWillAppear property to YES.

If you want a different behavior, you need to code it yourself. If the code you posted here is working to your liking, keep it. You could also modify the cell subclass to always pass YES to the superclass in the -setSelected:animated: method override.

0
matejmolnar On

I found a little workaround with prepareForReuse method which is called every time before the cell is reused:

class MyTableViewCell: UITableViewCell {

private var shouldAnimate = false

override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
    super.init(style: style, reuseIdentifier: reuseIdentifier)
    selectionStyle = .none
}

required init?(coder aDecoder: NSCoder) {
    fatalError("init(coder:) has not been implemented")
}

override func prepareForReuse() {
    shouldAnimate = false
}

override func setSelected(_ selected: Bool, animated: Bool) {
    super.setSelected(selected, animated: animated)

    if shouldAnimate {
        //your animation
    }
    else {
        shouldAnimate = true
    }
}

Hope it helps.

1
Jean Le Moignan On

Although being late in the discussion, here's a simple tweak to add in your cell code:

override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
    super.touchesEnded(touches, with: event)
    // Whatever you need to do here
    DispatchQueue.main.asyncAfter(deadline: .now() + 100.milliseconds) { self.setSelected(false, animated: true) }
}

Using touchesEnded instead of setSelected does the trick, either on the iPad or the iPhone.