drcocoa
drcocoa

Reputation: 1195

What is the proper way to implement selection and highlight animations on UITableViewCell?

I am trying to code a UITableViewCell with a custom animation for both selection and highlight states. I have overridden the setSelected:animated: and setHighlighted:animated: methods. However, these two methods are always called with animated:false. Therefore, I am unable to determine whether the methods are being invoked by the system (when the table view clears selections) or when the user taps. Now, for the highlight, I could probably just assume that it is always invoked by the user since I didn't find any other way to only highlight a cell programatically. For setSelected:animated:, I must know whether to animate or not. At first, I couldn't find what is calling setSelected:animated: in the first place when I tap on the cell because even after overriding every UITableViewDelegate method without calling super, setSelected:animated: was getting called from somewhere.

After searching for days, I disassembled the UIKitCore.framework using hopper disassembler and found out that setSelected:animated: is actually called by touchesEnded:event: method. There is actually an internal setSelected: method which calls setSelected:animated: with false by default. Moreover, since there is no documented api which lets you only highlight a cell (without selection) and internally, setHighlighted:animated: is always called with false, so what is the purpose of having an animated argument in this method's signature?

I have found not a whole lot of examples on this. Whatever I have found involves calling tableView.select:rowAtIndexPath:animated with animated:true manually in may be didSelectRow or willSelectRow. That feels hacky because it calls setSelected:animated: twice.

I am sure that I am not the first one to implement custom animations for highlight and selection for the UITableViewCell.

Update:

This is what I have implemented so far and it works even though it feels hacky.

override func setSelected(_ selected: Bool, animated: Bool) {
    guard isSelected != selected else {
        return
    }
    super.setSelected(selected, animated: animated)
    if animated {...}
}

// how can I call animated with true?
override func setHighlighted(_ highlighted: Bool, animated: Bool) {
    guard isHighlighted != highlighted else {
        return
    }
    super.setHighlighted(highlighted, animated: animated)
    if animated {...}
}


// overriding the touch handlers doesn't feel right.
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
    setHighlighted(true, animated: true)
    super.touchesBegan(touches, with: event)
}

override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
    setHighlighted(false, animated: true)
    setSelected(!isSelected, animated: true)
    super.touchesEnded(touches, with: event)
}

override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {
    setHighlighted(false, animated: true)
    super.touchesCancelled(touches, with: event)
}


Upvotes: 3

Views: 1447

Answers (2)

drcocoa
drcocoa

Reputation: 1195

I have come to the conclusion that there is really no API on UITableView to highlight the UITableViewCell programmatically like selection, unless you explicitly call setHighlighted:animated: on the cell instance with animated:true. I can confirm this after looking at the disassembled code for UITableView and UITableViewCell using Hopper.

In general, both setHighlighted:animated: and setSelected:animated: are invoked with animated:false from UIView touch handlers. There are two internal API function setHighlighted: and setAnimated: which internally pass the animated: false similar to the following:

func setSelected(_ selected: Bool) {
    setSelected(selected, animated: false)
}

func setHighlighted(_ highlighted: Bool) {
    setHighlighted(highlighted, animated: false)
}

func touches...(...) // the touch handlers {
    // setSelected(selected)
    // setHighlighted(highlighted) 
}

Overriding the touch handlers is not a proper solution because there are many other things happening the in the super implementation. If you skip them, it may corrupt the internal state of the cell. However, the moment you call super.touchesBegan or any other touch handler, the selection and highlight methods will be called with animated: false.

Moreover, from Apple's documentation it seems that you are supposed to override all the touch handlers if you decide to override one.

If you override this method without calling super (a common use pattern), you must also override the other methods for handling touch events, even if your implementations do nothing.

My Solution

func tableView(_ tableView: UITableView, willSelectRowAt indexPath: IndexPath) -> IndexPath? {
    tableView.selectRow(at: indexPath, animated: true, scrollPosition: .none)
    return indexPath
}

func tableView(_ tableView: UITableView, willDeselectRowAt indexPath: IndexPath) -> IndexPath? {
    tableView.deselectRow(at: indexPath, animated: true)
    return indexPath
}

You must return indexPath from these handlers as otherwise, the delegate methods will not be called. However, that means you will now call the setSelected:animated: twice. That can be prevented by using a guard as shown below.

override func setSelected(_ selected: Bool, animated: Bool) {
    guard isSelected != selected else {
        return
    }
    if animated {}
}

To animate highlight, use the following:

func tableView(_ tableView: UITableView, shouldHighlightRowAt indexPath: IndexPath) -> Bool {
    let cell = tableView.cellForRow(at: indexPath)

    // there is no api on tableview to highlight the cell.
    // so you must invoke highlight directly on the cell.
    cell?.setHighlighted(true, animated: true)
    return true
}

override func setHighlighted(_ highlighted: Bool, animated: Bool) {
    guard isHighlighted != highlighted else {
        return
    }
    if animated {}
}

However, this leaves one last hole, that is when highlighted == false, you wouldn't really know whether to animate it or not. You could keep a local variable in the cell to keep its track.

Upvotes: 0

Wolfgang
Wolfgang

Reputation: 132

I had an image in different cells in a collectionview once.

    let imageView: UIImageView = {
    let iv = UIImageView()
    iv.image = UIImage(named: "icon1")?.withRenderingMode(.alwaysTemplate)
    iv.tintColor = UIColor.rgb(red: 91, green: 14, blue: 13)
    return iv
}()
// Highlighted when pressed
override var isHighlighted: Bool {
    didSet {
        imageView.tintColor = isHighlighted ? UIColor.white : UIColor.rgb(red: 91, green: 14, blue: 13)
    }
}
// When selected
override var isSelected: Bool {
    didSet {
        imageView.tintColor = isSelected ? UIColor.white : UIColor.rgb(red: 91, green: 14, blue: 13)
    }
}

Maybe that gives you an idea.

Upvotes: 1

Related Questions