177
votes

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

20
What's upadteCell all about ?Idan
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
the accepted answer does not provide the solution, see my answer below for solutionPavel Gurov

20 Answers

226
votes

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
    }
}
121
votes

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

44
votes

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
10
votes

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.

9
votes

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];
8
votes

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

7
votes

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

4
votes

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
    }
}
4
votes

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...
    }
    
3
votes

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.

1
votes

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

        }
    }


}
1
votes

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
0
votes

Try Following code:

-(void)setHighlighted:(BOOL)highlighted animated:(BOOL)animated
{     
[super setHighlighted:highlighted animated:animated];
//Set your View's Color here.
}
0
votes

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

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
            }
        }
    }
}
0
votes

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
        }
    }
}
-1
votes

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

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)
    }
}
-1
votes

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
-1
votes

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.