UIView backgroundColor disappears when UITableView

2019-01-08 04:26发布

问题:

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 tableView is selected?

回答1:

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

  1. setHighlighted
  2. touchesEnded
  3. layoutSubviews
  4. willSelectRowAtIndexPath (delegate side)
  5. setSelected (!!! this is where all your view background colors are told to disappear)
  6. didSelectRowAtIndexPath (delegate side)
  7. setSelected (again) (Interestingly background colors not cleared on this call. What strangeness is going on inside that super method?)
  8. layoutSubviews (again)

So your options are to

  1. 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).
  2. 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 ...
  3. 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)



回答2:

- (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;
}


回答3:

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
    }

}


回答4:

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;
}


回答5:

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]];
}


回答6:

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.



回答7:

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 default UITableViewCell 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.



回答8:

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()
        })
    }
}


回答9:

Related to @Brooks's answer, this is what I did to make it work in Swift and iOS8/iOS9.

  • Override the setSelected and setHighlighted
  • 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()
        }
    }
    


回答10:

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.



回答11:

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.



回答12:

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



回答13:

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.



回答14:

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.



回答15:

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]
    }
}


回答16:

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
}