Reputation: 1195
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.
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
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.
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
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