UIView backgroundColor disappears when UITableViewCell is selected
I have a simple tableViewCell build in interface builder. It contains a UIView which contains an image. Now, when I select the cell, the default blue selection background is shown, but the backgroundColor of my UIView is gone.
My UITableViewCell's implementation file doesn't do anything special. It just init's & returns self and all I do in setSelected is call super.
How do I get my UIView backgroundColor to show when the tableV开发者_如何学Pythoniew is selected?
The problem here is that the [super] implementation of
- (void) setSelected:(BOOL) selected animated:(BOOL) animated;
sets all the background colors in the UITableViewCell to rgba(0,0,0,0). Why? Perhaps to make us all sweat?
It is not that entire views disappear (as evidenced by the fact that if you change the views layer border properties, those are retained)
Here is the sequence of function calls that results from touching a cell
- setHighlighted
- touchesEnded
- layoutSubviews
- willSelectRowAtIndexPath (delegate side)
- setSelected (!!! this is where all your view background colors are told to disappear)
- didSelectRowAtIndexPath (delegate side)
- setSelected (again) (Interestingly background colors not cleared on this call. What strangeness is going on inside that super method?)
- layoutSubviews (again)
So your options are to
- Override - (void) setSelected:(BOOL) selected animated:(BOOL) animated; without calling [super setSelected:selected animated:animated]. This will give you the most technically correct implementation because a) the code is wrapped up inside the UITableViewCell subclass and b) because it is only called when needed (well twice when needed, but maybe there is a way around that). The down side is you'll have to re-implement all the necessary functions (as opposed to unnecessary color clearing functions) of setSelected. Now don't ask me how to properly override setSelected just yet. Your guess is as good as mine for now (be patient, I'll edit this answer once I figure it out).
- Re-assert the background colors in didSelectRowAtIndexPath. This is not so great because it puts what should be instance code outside the instance. It has the upside that it is only called when it is needed, as opposed to ...
- Re-assert the background colors in layoutSubviews. This is not great at all because layoutSubviews is called like A MILLION times! It is called every time the table refreshes, every time it scrolls, every time you grandmother gets a perm... like seriously, a million times. That means there is a lot of unnecessary background re-assertions and a lot of extra processing overhead. On the bright side it puts the code inside the UITableViewCell subclass, which is nice.
Unfortunately re-asserting the background colors in setHighlighted does nothing because setHighlighted is called before all the background colors get set to [r:0 b:0 g:0 a:0] by the first call to setSelected.
//TODO: Give a great description of how to override setSelected (stay tuned)
- (void)setHighlighted:(BOOL)highlighted animated:(BOOL)animated
{
UIColor *backgroundColor = self.channelImageView.backgroundColor;
[super setHighlighted:highlighted animated:animated];
self.channelImageView.backgroundColor = backgroundColor;
}
- (void)setSelected:(BOOL)selected animated:(BOOL)animated
{
UIColor *backgroundColor = self.channelImageView.backgroundColor;
[super setSelected:selected animated:animated];
self.channelImageView.backgroundColor = backgroundColor;
}
When your UITableViewCell
is selected, there are two states you should pay attention to: Highlighted
and Selected
.
So, for scenarios that you have a custom cell class which is subclass of UITableViewCell
, you can easily override these two methods to avoid this situation(Swift):
class MyCell: UITableViewCell {
@IBOutlet var myView: UIView!
override func setHighlighted(highlighted: Bool, animated: Bool) {
let myViewBackgroundColor = myView.backgroundColor
super.setHighlighted(highlighted, animated: animated)
myView.backgroundColor = myViewBackgroundColor
}
override func setSelected(selected: Bool, animated: Bool) {
let myViewBackgroundColor = myView.backgroundColor
super.setSelected(selected, animated: animated)
myView.backgroundColor = myViewBackgroundColor
}
}
Previously I have done as @P5ycH0 said (1x1 image stretched), but following @Brooks I figured that overriding -(void)setHighlighted:(BOOL)highlighted animated:(BOOL)animated
in my custom UITableViewCell
implementation and resetting the the background colors after calling [super setHighlighted:highlighted animated:animated];
keeps my background colors when the cell is selected/highlighted
-(void)setHighlighted:(BOOL)highlighted animated:(BOOL)animated {
[super setHighlighted:highlighted animated:animated];
myView.backgroundColor = myColor;
}
This issue may (finally) be resolved in iOS 13. Found this sweet paragraph in the iOS 13 beta 3 release notes.
The UITableViewCell class no longer changes the backgroundColor or isOpaque properties of the contentView and any of its subviews when cells become highlighted or selected. If you are setting an opaque backgroundColor on any subviews of the cell inside (and including) the contentView, the appearance when the cell becomes highlighted or selected might be affected. The simplest way to resolve any issues with your subviews is to ensure their backgroundColor is set to nil or clear, and their opaque property is false. However, if needed you can override the setHighlighted(:animated:) and setSelected(:animated:) methods to manually change these properties on your subviews when moving to or from the highlighted and selected states. (13955336)
https://developer.apple.com/documentation/ios_ipados_release_notes/ios_ipados_13_beta_3_release_notes
Brooks has a great explanation for why this happens, but I think I have a better solution.
In your subview, override setBackgroundColor:
to what ever color you want. The setter will still be called, but only your color specified will be enforced.
- (void)setBackgroundColor:(UIColor *)backgroundColor {
[super setBackgroundColor:[UIColor whiteColor]];
}
Ok, loosing the background color of a UIView class is normal behavior when its in a selected tableviewcell. I couldn't figure out how to prevent that. Now I've just replaced the UIView with an UIImageView containing a stretched 1x1 white pixel. Ugly imo, but it works.
You need to override the next two methods in your custom cell:
- (void) setSelected:(BOOL)selected animated:(BOOL)animated;
- (void) setHighlighted:(BOOL)highlighted animated:(BOOL)animated;
Note that:
- you should call
[super setSelected:animated:]
and[super setHighlighted:animated:]
in the beginning of your custom implementation or correspond methods; - you should set the
UITableViewCellSelectionStyleNone
selectionStyle for your custom cell, to disable any defaultUITableViewCell
styling;
Here the example of the implementation:
- (void) setHighlighted:(BOOL)highlighted animated:(BOOL)animated
{
[super setHighlighted:highlighted animated:animated];
[self setHighlightedSelected:highlighted animated:animated];
}
- (void) setSelected:(BOOL)selected animated:(BOOL)animated
{
[super setSelected:selected animated:animated];
[self setHighlightedSelected:selected animated:animated];
}
- (void) setHighlightedSelected:(BOOL)selected animated:(BOOL)animated
{
void(^selection_block)(void) =
^
{
self.contentView.backgroundColor = selected ? SELECTED_BACKGROUND_COLOR : NORMAL_BACKGROUND_COLOR;
};
if(animated)
{
[UIView animateWithDuration:SELECTION_ANIMATION_DURATION
delay:0.0
options:UIViewAnimationOptionBeginFromCurrentState
animations:selection_block
completion:NULL];
}
else
selection_block();
}
The contentView
is the property of UITableViewCell
that is appeared in iOS 7. Note that you can use your own cell's child view or views instead of it.
Add this to your UITableViewCell
override func setHighlighted(highlighted: Bool, animated: Bool) {
super.setHighlighted(false, animated: animated)
if highlighted {
self.backgroundColor = UIColor.blueColor()
}else{
UIView.animateWithDuration(0.2, animations: {
self.backgroundColor = UIColor.clearColor()
})
}
}
Related to @Brooks's answer, this is what I did to make it work in Swift and iOS8/iOS9.
- Override the
setSelected
andsetHighlighted
- Don't call super
- Clear out the
contentView.backgroundColor
, because it does not have to span over the whole width of the cell (i.e. accessories). Use the
backgroundColor
of the cell itself, and set it accordingly.class AwesomeTableViewCell: UITableViewCell { private struct Constants { static var highlightedColor = UIColor.greenColor() static var selectedColor = UIColor.redColor() static let animationTime = NSTimeInterval(0.2) } override func awakeFromNib() { super.awakeFromNib() contentView.backgroundColor = UIColor.clearColor() backgroundColor = AppContext.sharedInstance.theme.colors.background } override func setHighlighted(highlighted: Bool, animated: Bool) { if animated { UIView.animateWithDuration(Constants.animationTime, animations: { () -> Void in self.setHighlighted(highlighted) }) } else { self.setHighlighted(highlighted) } } override func setSelected(selected: Bool, animated: Bool) { if animated { UIView.animateWithDuration(Constants.animationTime, animations: { () -> Void in self.setSelected(selected) }) } else { self.setSelected(selected) } } private func setHighlighted(highlighted: Bool) { backgroundColor = highlighted ? Constants.highlightedColor : UIColor.whiteColor() } private func setSelected(selected: Bool) { backgroundColor = selected ? Constants.selectedColor : UIColor.whiteColor() } }
Summary
This solution let's you lock some of a cell's background colors, while the remainder are controlled by system behaviour.
Based on mientus' answer, I have created a solution which allows you to specify which views should keep their background color.
This still allows other cell subviews to have their background removed on highlighting/selection, and is the only solution which works in our case (two views needing a permanent background).
I used a protocol-oriented approach, with a BackgroundLockable
protocol containing the list of views to lock, and running a closure while keeping the colors:
protocol BackgroundLockable {
var lockedBackgroundViews: [UIView] { get }
func performActionWithLockedViews(_ action: @escaping () -> Void)
}
extension BackgroundLockable {
func performActionWithLockedViews(_ action: @escaping () -> Void) {
let lockedViewToColorMap = lockedBackgroundViews.reduce([:]) { (partialResult, view) -> [UIView: UIColor?] in
var mutableResult = partialResult
mutableResult[view] = view.backgroundColor
return mutableResult
}
action()
lockedViewToColorMap.forEach { (view: UIView, color: UIColor?) in
view.backgroundColor = color
}
}
}
Then I have a subclass of UITableViewCell
, which overrides highlighting and selection to run the protocol's closure around calling the default (super) behaviour:
class LockableBackgroundTableViewCell: UITableViewCell, BackgroundLockable {
var lockedBackgroundViews: [UIView] {
return []
}
override func setHighlighted(_ highlighted: Bool, animated: Bool) {
performActionWithLockedViews {
super.setHighlighted(highlighted, animated: animated)
}
}
override func setSelected(_ selected: Bool, animated: Bool) {
performActionWithLockedViews {
super.setSelected(selected, animated: animated)
}
}
}
Now I just have to subclass LockableBackgroundTableViewCell
or use the BackgroundLockable
protocol in a cell class to easily add locking behaviour to some cells!
class SomeCell: LockableBackgroundTableViewCell {
@IBOutlet weak var label: UILabel!
@IBOutlet weak var icon: UIImageView!
@IBOutlet weak var button: UIButton!
override var lockedBackgroundViews: [UIView] {
return [label, icon]
}
}
Swift 4
In your UITableViewCell class:
override func setSelected(_ selected: Bool, animated: Bool) {
myView.backgroundColor = UIColor.blue
}
override func setHighlighted(_ highlighted: Bool, animated: Bool) {
myView.backgroundColor = UIColor.blue
}
From that you said you built a tableViewCell using IB, I'd like to check whether you are adding your view as a subview of contentView
of UITableViewCell, not view
. The content view is the default superview for content displayed by the cell.
From the reference:
The content view of a UITableViewCell object is the default superview for content displayed by the cell. If you want to customize cells by simply adding additional views, you should add them to the content view so they will be positioned appropriately as the cell transitions into and out of editing mode.
You can change the behavior of the tableViewCell by overriding the function setHighlighted in UITableViewCell class (you will need to inherit from it). Example of my code where I change the background image of my cell :
// animate between regular and highlighted state
- (void)setHighlighted:(BOOL)highlighted animated:(BOOL)animated; {
[super setHighlighted:highlighted animated:animated];
//Set the correct background Image
UIImageView* backgroundPicture = (UIImageView*)[self viewWithTag:HACK_BACKGROUND_VIEW_TAG];
if (highlighted) {
backgroundPicture.image = [UIImage imageNamed:@"FondSelected.png"];
}
else {
backgroundPicture.image = [UIImage imageNamed:@"Fond.png"];
}
}
You can also change the selection mode to gray, blue or none in the interface builder.
In iOS 7, what worked for me is to override setSelected:animated:
in the UITableViewCell
subclass, but contrary to @Brooks' tip, I called [super setSelected:selected animated:animated]
.
- (void)setSelected:(BOOL)selected animated:(BOOL)animated
{
[super setSelected:selected animated:animated];
// Reassert the background color of the color view so that it shows
// even when the cell is highlighted for selection.
self.colorView.backgroundColor = [UIColor blueColor];
}
This lets me keep the system's default selection animation when the user taps on the cell, and also to deselect it in the table view delegate's didSelectRowAtIndexPath:
.
Just spent some time on this weird issue. I did not want to set UITableViewCellSelectionStyleNone style to preserve nice animation when my row was selected. But none of suggested ideas worked for me - I was trying to override setSelected and setHighlighted and set my subview backgroundColor there - it was keeping resetting by iOS and still blinking (new color -> old color). For me the fix was quite simple. When my row is selected another view controller is pushed, user chooses some option on that screen and delegate is called where I change the color based on user selection. In this delegate I just do [cell setSelected:NO animated:NO] for my cell. (I have static UITableViewController and have outlets to cells). You could probably deselect cell in didSelect method but in my case I'm using segues.
Here's my take on it. I have a subclass that all my cell inherit from, so that's the way I do it to avoid the background change in my UIImageViews:
override func setHighlighted(highlighted: Bool, animated: Bool) {
var backgroundColors = [UIView: UIColor]()
for view in contentView.subviews as [UIView] {
if let imageView = view as? UIImageView {
backgroundColors[imageView] = imageView.backgroundColor
}
}
super.setHighlighted(highlighted, animated: animated)
for view in contentView.subviews as [UIView] {
if let imageView = view as? UIImageView {
imageView.backgroundColor = backgroundColors[imageView]
}
}
}
override func setSelected(selected: Bool, animated: Bool) {
var backgroundColors = [UIView: UIColor]()
for view in contentView.subviews as [UIView] {
if let imageView = view as? UIImageView {
backgroundColors[imageView] = imageView.backgroundColor
}
}
super.setSelected(selected, animated: animated)
for view in contentView.subviews as [UIView] {
if let imageView = view as? UIImageView {
imageView.backgroundColor = backgroundColors[imageView]
}
}
}
This automaticlly fix the issue for all UIImageView
.
精彩评论