177

I'm implementing a color-chooser table view where the user can select amongst, say, 10 colors (depends on the product). The user can also select other options (like hard drive capacity, ...).

All color options are inside their own tableview section.

I want to display a little square on the left of the textLabel showing the actual color.

Right now I'm adding a simple square UIView, give it the correct background color, like this :

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:RMProductAttributesCellID];
    if (cell == nil) {
        cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleValue1 reuseIdentifier:RMProductAttributesCellID] autorelease];
        cell.indentationWidth = 44 - 8;

        UIView *colorThumb = [[[UIView alloc] initWithFrame:CGRectMake(8, 8, 28, 28)] autorelease];
        colorThumb.tag = RMProductAttributesCellColorThumbTag;
        colorThumb.hidden = YES;
        [cell.contentView addSubview:colorThumb];
    }

    RMProductAttribute *attr = (RMProductAttribute *)[_product.attributes objectAtIndex:indexPath.section];
    RMProductAttributeValue *value = (RMProductAttributeValue *)[attr.values objectAtIndex:indexPath.row];
    cell.textLabel.text = value.name;
    cell.textLabel.backgroundColor = [UIColor clearColor];

    UIView *colorThumb = [cell viewWithTag:RMProductAttributesCellColorThumbTag];
    colorThumb.hidden = !attr.isColor;
    cell.indentationLevel = (attr.isColor ? 1 : 0);

    if (attr.isColor) {
        colorThumb.layer.cornerRadius = 6.0;
        colorThumb.backgroundColor = value.color;
    }

    [self updateCell:cell atIndexPath:indexPath];

    return cell;
}

This displays fine without problems.

My only problem is that when I select a "color" row, during the fade-to-blue selection animation, my custom UIView (colorThumb) is hidden. It gets visible again just after the selection/deselection animation ended, but this produces an ugly artifact.

What should I do to correct this? Don't I insert the subview at the right place?

(There's nothing special in the didSelectRowAtIndexPath, I just change the cell's accessory to a checkbox or nothing, and deselect the current indexPath).

Cyrille
  • 25,014
  • 12
  • 67
  • 90
  • What's upadteCell all about ? – Idan Jul 19 '11 at 11:11
  • updateCell does some minor tweaks like setting the checkmark or not, choosing text color depending on availability, ... but no real change related to the cell itself or the colorThumb. – Cyrille Jul 19 '11 at 12:06
  • the accepted answer does not provide the solution, see my answer below for solution – Pavel Gurov Mar 22 '16 at 18:56

20 Answers20

225

UITableViewCell changes the background color of all sub views when cell is selected or highlighted ,You can Solve this problem by overriding Tableview cell's setSelected:animated and setHighlighted:animated and resetting view background color.

In Objective C :

- (void)setSelected:(BOOL)selected animated:(BOOL)animated {
   UIColor *color = self.yourView.backgroundColor;        
   [super setSelected:selected animated:animated];

    if (selected){
        self.yourView.backgroundColor = color;
    }
}

-(void)setHighlighted:(BOOL)highlighted animated:(BOOL)animated{
    UIColor *color = self.yourView.backgroundColor;        
    [super setHighlighted:highlighted animated:animated];

    if (highlighted){
        self.yourView.backgroundColor = color;
    }
}

In Swift 3.1 :

override func setSelected(_ selected: Bool, animated: Bool) {
    let color = yourView.backgroundColor         
    super.setSelected(selected, animated: animated)

    if selected {
        yourView.backgroundColor = color
    }
}

override func setHighlighted(_ highlighted: Bool, animated: Bool) {
    let color = yourView.backgroundColor
    super.setHighlighted(highlighted, animated: animated)

    if highlighted {
        yourView.backgroundColor = color
    }
}
Søren Mortensen
  • 1,663
  • 1
  • 11
  • 25
Yatheesha
  • 10,412
  • 5
  • 42
  • 45
  • Do we need the `if (highlighted)` and `if (selected)` conditions? I think it will work if we don't have those conditions. – Rishabh Tayal Oct 21 '16 at 04:10
  • @RishabhTayal it is basically to avoid overriding the variable with the same value – cameloper Feb 12 '18 at 16:19
  • 1
    Mind that when you change the background color while the item is selected, the old (wrong) color might be restored when the item becomes unselected. If that can happen, remove the if (highlighted) and if (selected) conditions. – Marcel Wolterbeek Jun 28 '18 at 09:51
  • This approach cancels the animation, so it has no sense. Better to set cell selection style to .none. – Alexander Danilov Mar 14 '19 at 19:42
121

It's because the table view cell automatically changes the background color of all views inside the content view for the highlighted state. You may consider subclassing UIView to draw your color or using UIImageView with a custom 1x1 px stretched image.

Andriy
  • 2,767
  • 2
  • 21
  • 29
  • 1
    Dumb me. Of course that was it, subviews have to be transparent so that the selection animation can occur properly. Thanks! – Cyrille Jul 19 '11 at 12:06
  • 52
    Or you can re-set the background color overriding `setHighlighted:animated:` and `setSelected:animated:` – ySgPjx Mar 01 '12 at 17:41
  • 1
    Somehow resetting the background color did not work for me (running on iOS 8.1). Instead solved this by subclassing my view and overriding setBackgroundColor as [super setBackgroundColor:[UIColor whiteColor]]. – bizz84 Nov 20 '14 at 14:54
44

Found a pretty elegant solution instead of messing with the tableViewCell selection/highlighting methods. You can create a subclass of UIView that ignores setting its background color to clear color.

Swift 3/4:

class NeverClearView: UIView {
    override var backgroundColor: UIColor? {
        didSet {
            if backgroundColor != nil && backgroundColor!.cgColor.alpha == 0 {
                backgroundColor = oldValue
            }
        }
    }
}

Swift 2:

class NeverClearView: UIView {
    override var backgroundColor: UIColor? {
        didSet {
            if CGColorGetAlpha(backgroundColor!.CGColor) != 0 {
                backgroundColor = oldValue
            }
        }
    }
}

Obj-C version:

@interface NeverClearView : UIView

@end

@implementation NeverClearView

- (void)setBackgroundColor:(UIColor *)backgroundColor {
    if (CGColorGetAlpha(backgroundColor.CGColor) != 0) {
        [super setBackgroundColor:backgroundColor];
    }
}

@end
Mark
  • 681
  • 7
  • 15
Pavel Gurov
  • 5,587
  • 3
  • 26
  • 23
  • This is lovely. Easily the best solution if you have some re-usable view like a "badge" or "tag" that should just never have a clear background. ::rant:: what a confounding solution @UIKit, setting all child views to transparent when you do a cell selection. At least limit to child views that are the full height or width of the cell, or ones at N depth. – SimplGy May 11 '16 at 14:52
  • @SimplGy The depth of highlighting would be indeed a sweet option, but hey - this is UIKit, I have seen things SO much worse than this =) – Pavel Gurov May 18 '16 at 06:36
  • 3
    Worked for me after I changed the if to CGColorGetAlpha(backgroundColor!.CGColor) == 0 as it wasn't equal to clearColor – piltdownman7 Jun 22 '16 at 20:10
  • Great solution! No longer necessary with the iOS 13 SDK. – Mark Aug 23 '20 at 20:28
10

For Swift 2.2 this works

cell.selectionStyle = UITableViewCellSelectionStyle.None

and reason is explained by @Andriy

It's because table view cell automatically changes background color of all views inside content view for highlighted state.

Community
  • 1
  • 1
swiftBoy
  • 35,607
  • 26
  • 136
  • 135
9

Another way to manage the problem is to fill the view with core-graphics gradient, like:

CAGradientLayer* gr = [CAGradientLayer layer];
gr.frame = mySubview.frame;
gr.colors = [NSArray arrayWithObjects:
                     (id)[[UIColor colorWithRed:0 green:0 blue:0 alpha:.5] CGColor]
                     ,(id)[[UIColor colorWithRed:0 green:0 blue:0 alpha:.5] CGColor]
                     , nil];

gr.locations = [NSArray arrayWithObjects:[NSNumber numberWithFloat:0],[NSNumber numberWithFloat:1],nil];

[mySubview.layer insertSublayer:gr atIndex:0];
Agat
  • 4,577
  • 2
  • 34
  • 62
  • Hmm, I'm trying this exact code and it isn't having any effect at all for me. My subview is a UILabel added as a subview of cell.contentView, and testing under iOS 6.0.1, in case that matters. – Joe Strout Jun 23 '13 at 23:31
  • What do you apply the code above to? And have you tried to add the label simply to the cell view? – Agat Jun 23 '13 at 23:38
  • This is the perfect solution in my opinion. Drawing to the layer addresses the issue perfectly, while keeping full flexibility. I don't like the solution of using a UIImageView, because then it's harder to adjust the gradient or the color (you'd have to make a new image each time), and subclassing UIView just for this seems overkill. – Erik van der Neut Jul 01 '14 at 07:28
  • @Lyida, I am not really Swift-developer. (I even more C#-one). But from what I've seen, that's not rather language-specific thing, however, mostly Cocoa/iOS Frameworks logic one. So, the idea is just to place almost transparent CAGradientLayer into your view to obtain the result requested. – Agat May 28 '15 at 11:04
8

Inspired by Yatheesha B L's answer I created a UITableViewCell category/extension that allows you to turn on and off this transparency "feature".

Swift

let cell = <Initialize Cell>
cell.keepSubviewBackground = true  // Turn  transparency "feature" off
cell.keepSubviewBackground = false // Leave transparency "feature" on

Objective-C

UITableViewCell* cell = <Initialize Cell>
cell.keepSubviewBackground = YES;  // Turn  transparency "feature" off
cell.keepSubviewBackground = NO;   // Leave transparency "feature" on

KeepBackgroundCell is CocoaPods compatible. You can find it on GitHub

Community
  • 1
  • 1
Tim Bodeit
  • 9,673
  • 3
  • 27
  • 57
7

You can cell.selectionStyle = UITableViewCellSelectionStyleNone;, then set the backgroundColor at - (void)tableView:(UITableView *)tableView didHighlightRowAtIndexPath:(NSIndexPath *)indexPath

gumpwang
  • 109
  • 2
  • 5
4

Inspired by Yatheesha B L's answer.

If you call super.setSelected(selected, animated:animated), it will clear all background color you set. So, we will not call super method.

In Swift :

override func setSelected(selected: Bool, animated: Bool) {    
    if(selected)  {
        contentView.backgroundColor = UIColor.red 
    } else {
        contentView.backgroundColor = UIColor.white
    }
}

override func setHighlighted(highlighted: Bool, animated: Bool) {
    if(highlighted) {
        contentView.backgroundColor = UIColor.red 
    } else {
        contentView.backgroundColor = UIColor.white
    }
}
Community
  • 1
  • 1
Milan Kamilya
  • 2,188
  • 1
  • 31
  • 44
  • 1
    Thanks for the solution. +1 Overriding the ishiglighted variable and sethighlighted method are different things. the correct answer is to override the method. – Numan Karaaslan Sep 03 '17 at 10:56
4

For may case, This is the Septs to avoid getting the gray color for all items in the cell (in case you are using custom table view cell):

  1. Set the selectionStyle to .none selectionStyle = .none

  2. Override this method.

    func setHighlighted(_ highlighted: Bool, animated: Bool)

  3. Call the super, to get the benefit of super setup.

    super.setHighlighted(highlighted, animated: animated)

  4. Do what ever highlighting logic you want.

    override func setHighlighted(_ highlighted: Bool, animated: Bool) {
          super.setHighlighted(highlighted, animated: animated)
          // Your Highlighting Logic goes here...
    }
    
Atef
  • 2,872
  • 1
  • 36
  • 32
3

UITableViewCell changes the backgroundColor of all subviews on selection for some reason.

This might help:

DVColorLockView

Use something like that to stop UITableView from changing your view color during selection.

DylanVann
  • 483
  • 5
  • 9
1

Draw the view instead of setting background colour

import UIKit

class CustomView: UIView {

    var fillColor:UIColor!

    convenience init(fillColor:UIColor!) {
        self.init()
        self.fillColor = fillColor
    }

    override func drawRect(rect: CGRect) {
        if let fillColor = fillColor {
            let context = UIGraphicsGetCurrentContext()
            CGContextSetFillColorWithColor(context, fillColor.CGColor);
            CGContextFillRect (context, self.bounds);

        }
    }


}
PeiweiChen
  • 413
  • 5
  • 16
1

SIMPLEST solution without bugs with animation (as in the top rated answer) and without subclassing and drawing - set layer's border color instead of backgroundColor and set very big border width.

colorThumb.layer.cornerRadius = 6
colorThumb.layer.borderWidth = colorThumb.frame.width
colorThumb.layer.borderColor = value.color
Alexander Danilov
  • 3,038
  • 1
  • 30
  • 35
0

Try Following code:

-(void)setHighlighted:(BOOL)highlighted animated:(BOOL)animated
{     
[super setHighlighted:highlighted animated:animated];
//Set your View's Color here.
}
Mehul Thakkar
  • 12,440
  • 10
  • 52
  • 81
0

Don't forget to override setSelected as well as setHighlighted

override func setHighlighted(highlighted: Bool, animated: Bool) {

    super.setHighlighted(highlighted, animated: animated)
    someView.backgroundColor = .myColour()
}

override func setSelected(selected: Bool, animated: Bool) {

    super.setSelected(selected, animated: animated)
    someView.backgroundColor = .myColour()
}
Magoo
  • 2,552
  • 1
  • 23
  • 43
0

This is similar to Pavel Gurov's answer, but more flexible in that it allows any color to be permanent.

class PermanentBackgroundColorView: UIView {
    var permanentBackgroundColor: UIColor? {
        didSet {
            backgroundColor = permanentBackgroundColor
        }
    }

    override var backgroundColor: UIColor? {
        didSet {
            if backgroundColor != permanentBackgroundColor {
                backgroundColor = permanentBackgroundColor
            }
        }
    }
}
user3352495
  • 374
  • 3
  • 11
0

I wanted to keep the default selection behavior except for one cell subview that I wanted to ignore the automatic background color change. But I also needed to be able to change the background color at other times.

The solution I came up with was to subclass UIView so it ignores setting the background color normally and add a separate function to bypass the protection.

Swift 4

class MyLockableColorView: UIView {
    func backgroundColorOverride(_ color: UIColor?) {
            super.backgroundColor = color
    }

    override var backgroundColor: UIColor? {
        set {
            return
        }
        get {
            return super.backgroundColor
        }
    }
}
zekel
  • 9,227
  • 10
  • 65
  • 96
-1

here is my solution,use contentView to show selectionColor,it's work perfectly

#import "BaseCell.h"

@interface BaseCell ()
@property (nonatomic, strong) UIColor *color_normal;
@property (nonatomic, assign) BOOL needShowSelection;
@end


@implementation BaseCell
@synthesize color_customSelection;
@synthesize color_normal;
@synthesize needShowSelection;

- (void)awakeFromNib
{
    [super awakeFromNib];
    [self setup];
}

- (void)setup
{
    //save normal contentView.backgroundColor
    self.color_normal = self.backgroundColor;
    if (self.color_normal == nil) {
        self.color_normal = [UIColor colorWithRGBHex:0xfafafa];
    }
    self.color_customSelection = [UIColor colorWithRGBHex:0xF1F1F1];
    self.accessoryView.backgroundColor = [UIColor clearColor];
    if (self.selectionStyle == UITableViewCellSelectionStyleNone) {
        needShowSelection = NO;
    }
    else {
        //cancel the default selection
        needShowSelection = YES;
        self.selectionStyle = UITableViewCellSelectionStyleNone;
    }
}

/*
 solution is here
 */
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
    [super touchesBegan:touches withEvent:event];
    if (needShowSelection) {
        self.contentView.backgroundColor = self.backgroundColor = color_customSelection;
    }
}

- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event
{
    [super touchesCancelled:touches withEvent:event];
    if (needShowSelection) {
        self.contentView.backgroundColor = self.backgroundColor = color_normal;
    }
}

- (void)setSelected:(BOOL)selected animated:(BOOL)animated
{
    [super setSelected:selected animated:animated];
    if (needShowSelection) {
        UIColor *color  = selected ? color_customSelection:color_normal;
        self.contentView.backgroundColor = self.backgroundColor = color;
    }
}
P.J.Radadiya
  • 1,493
  • 1
  • 12
  • 21
Nick
  • 241
  • 1
  • 2
  • 9
-1

Place this code in your subclass of UITableViewCell

Swift 3 syntax

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

    if(selected) {
        lockerSmall.backgroundColor = UIColor.init(red: 233/255, green: 106/255, blue: 49/255, alpha: 1.0)
    }
}


override func setHighlighted(_ highlighted: Bool, animated: Bool) {
    super.setHighlighted(highlighted, animated: animated)

    if(highlighted) {
        lockerSmall.backgroundColor = UIColor.init(red: 233/255, green: 106/255, blue: 49/255, alpha: 1.0)
    }
}
Jay Mayu
  • 17,023
  • 32
  • 114
  • 148
-1

Adding another solution if you're using storyboards. Create a subclass of UIView that does not allow the backgroundColor to be set after it is initially set.

@interface ConstBackgroundColorView : UIView

@end

@implementation ConstBackgroundColorView

- (void)setBackgroundColor:(UIColor *)backgroundColor {
    if (nil == self.backgroundColor) {
        [super setBackgroundColor:backgroundColor];
    }
}

@end
mofojed
  • 1,342
  • 9
  • 11
-1

If the background solution mentioned above isn't fixing your problem, your issue may lie in your datasource for your tableView.

For me, I was creating an instance of a DataSource object (called BoxDataSource) to handle the delegate and dataSource tableView methods, as so:

//In cellForRowAtIndexPath, when setting up cell
let dataSource = BoxDataSource(delegate: self)
cell.tableView.dataSource = dataSource
return cell

This was causing the dataSource to be deallocated whenever the cell was tapped, and thus all the contents disappeared. The reason being, is ARC deallocating/garbage collecting nature.

To fix this, I had to go into the custom cell, add a datasource variable:

//CustomCell.swift
var dataSource: BoxDataSource?

Then, you need to set the dataSource to the cell's dataSource var you just created in cellForRow, so this isnt deallocated with ARC.

cell.statusDataSource = BoxAssigneeStatusDataSource(delegate: self)
cell.detailsTableView.dataSource = cell.statusDataSource
return cell

Hope that helps.

Josh O'Connor
  • 4,694
  • 7
  • 54
  • 98