41
votes

My current project's UITableViewCell behavior is baffling me. I have a fairly straightforward subclass of UITableViewCell. It adds a few extra elements to the base view (via [self.contentView addSubview:...] and sets background colors on the elements to have them look like black and grey rectangular boxes.

Because the background of the entire table has this concrete-like texture image, each cell's background needs to be transparent, even when selected, but in that case it should darken a bit. I've set a custom semi-transparent selected background to achieve this effect:

UIView *background = [[[UIView alloc] initWithFrame:self.bounds] autorelease];
background.backgroundColor = [[UIColor blackColor] colorWithAlphaComponent:0.6];
background.opaque = NO;

[self setSelectedBackgroundView:background];

And although that yields the right look for the background, a weird side effect happens when I select the cell; all other backgrounds are somehow turnt off. Here's a screenshot. The bottom cell looks like it should and is not selected. The top cell is selected, but it should display the black and grey rectangular areas, yet they are gone!

Screenshot of the simulator. The top cell is selected, the bottom is not.

Who knows what's going on here and even more important: how can I correct this?

8
I know that one solution would be to get rid of all the subviews and draw everything 'manually' in the drawRect: method, but that's not an option due to classes that depend on this mechanism right now.epologee
Good suggestion, @ms83, but the end result is exactly the same, just that now there's an image on the background. Still all other backgrounds 'disappear'. It's not like they get set to that background as well, for then the multiple transparent views should stack and still be visible, even if in a not-intended way.epologee
This sounds like a caching issue. Perhaps create two CellIdentifiers, one for a selected cell, another one for a non-selected cell. Initialize either type of cell and dequeue as appropriate.Wolfgang Schreurs
Hi @Wolfgang, I don't quite get what you mean. What you're seeing in the image are just the selected and regular state of one and the same cell. The selection is due to me holding the mouse button (simulator) while taking the screenshot. Same thing happens on device.epologee

8 Answers

55
votes

What is happening is that each subview inside the TableViewCell will receive the setSelected and setHighlighted methods. The setSelected method will remove background colors but if you set it for the selected state it will be corrected.

For example if those are UILabels added as subviews in your customized cell, then you can add this to the setSelected method of your TableViewCell implementation code:

- (void)setSelected:(BOOL)selected animated:(BOOL)animated
{
    [super setSelected:selected animated:animated];

    self.textLabel.backgroundColor = [UIColor blackColor];

}

where self.textLabel would be whatever those labels are that are shown in the picture above

I'm not sure where your adding your selected view, I usually add it in the setSelected method.

Alternatively, you can subclass the UILabel and override the setHighlighted method like so:

-(void)setHighlighted:(BOOL)highlighted
{
    [self setBackgroundColor:[UIColor blackColor]];
}
42
votes

The cell highlighting process can seem complex and confusing if you don't know whats going on. I was thoroughly confused and did some extensive experimentation. Here's the notes on my findings that may help somebody (if anyone has anything to add to this or refute then please comment and I will endeavour to confirm and update)

In the normal “not selected” state

  • The contentView (whats in your XIB unless you coded it otherwise) is drawn normally
  • The selectedBackgroundView is HIDDEN
  • The backgroundView is visible (so provided your contentView is transparent you see the backgroundView or (if you have not defined a backgroundView you'll see the background colour of the UITableView itself)

A cell is selected, the following occurs immediately with-OUT any animation:

  • All views/subviews within the contentView have their backgroundColor cleared (or set to transparent), label etc text color's change to their selected colour
  • The selectedBackgroundView becomes visible (this view is always the full size of the cell (a custom frame is ignored, use a subview if you need to). Also note the backgroundColor of subViews are not displayed for some reason, perhaps they're set transparent like the contentView). If you didn't define a selectedBackgroundView then Cocoa will create/insert the blue (or gray) gradient background and display this for you)
  • The backgroundView is unchanged

When the cell is deselected, an animation to remove the highlighting starts:

  • The selectedBackgroundView alpha property is animated from 1.0 (fully opaque) to 0.0 (fully transparent).
  • The backgroundView is again unchanged (so the animation looks like a crossfade between selectedBackgroundView and backgroundView)
  • ONLY ONCE the animation has finished does the contentView get redrawn in the "not-selected" state and its subview backgroundColor's become visible again (this can cause your animation to look horrible so it is advisable that you don't use UIView.backgroundColor in your contentView)

CONCLUSIONS:

If you need a backgroundColor to persist through out the highlight animation, don't use the backgroundColor property of UIView instead you can try (probably with-in tableview:cellForRowAtIndexPath:):

A CALayer with a background color:

UIColor *bgColor = [UIColor greenColor];
CALayer* layer = [CALayer layer];
layer.frame = viewThatRequiresBGColor.bounds;
layer.backgroundColor = bgColor.CGColor;
[cell.viewThatRequiresBGColor.layer addSublayer:layer];

or a CAGradientLayer:

UIColor *startColor = [UIColor redColor];
UIColor *endColor = [UIColor purpleColor];
CAGradientLayer* gradientLayer = [CAGradientLayer layer];
gradientLayer.frame = viewThatRequiresBGColor.bounds;
gradientLayer.colors = @[(id)startColor.CGColor, (id)endColor.CGColor];
gradientLayer.locations = @[[NSNumber numberWithFloat:0],[NSNumber numberWithFloat:1]];
[cell.viewThatRequiresBGColor.layer addSublayer:gradientLayer];

I've also used a CALayer.border technique to provide a custom UITableView seperator:

// We have to use the borderColor/Width as opposed to just setting the 
// backgroundColor else the view becomes transparent and disappears during 
// the cell's selected/highlighted animation
UIView *separatorView = [[UIView alloc] initWithFrame:CGRectMake(0, 43, 1024, 1)];
separatorView.layer.borderColor = [UIColor redColor].CGColor;
separatorView.layer.borderWidth = 1.0;
[cell.contentView addSubview:separatorView];
17
votes

When you start dragging a UITableViewCell, it calls setBackgroundColor: on its subviews with a 0-alpha color. I worked around this by subclassing UIView and overriding setBackgroundColor: to ignore requests with 0-alpha colors. It feels hacky, but it's cleaner than any of the other solutions I've come across.

@implementation NonDisappearingView

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

@end

Then, I add a NonDisappearingView to my cell and add other subviews to it:

-(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    static NSString *cellIdentifier = @"cell";    
    UITableViewCell *cell = [self.tableView dequeueReusableCellWithIdentifier:cellIdentifier];
    if (cell == nil) {
        cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:cellIdentifier] autorelease];
        UIView *background = [cell viewWithTag:backgroundTag];
        if (background == nil) {
            background = [[NonDisappearingView alloc] initWithFrame:backgroundFrame];
            background.tag = backgroundTag;
            background.backgroundColor = backgroundColor;
            [cell addSubview:background];
        }

        // add other views as subviews of background
        ...
    }
    return cell;
}

Alternatively, you could make cell.contentView an instance of NonDisappearingView.

6
votes

My solution is saving the backgroundColor and restoring it after the super call.

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

You also need to do the same thing with -setHighlighted:animated:.

4
votes

Found a pretty elegant solution instead of messing with the tableView methods. You can create a subclass of UIView that ignores setting its background color to clear color. Code:

class NeverClearView: UIView {
    override var backgroundColor: UIColor? {
        didSet {
            if UIColor.clearColor().isEqual(backgroundColor) {
                backgroundColor = oldValue
            }
        }
    }
}

Obj-C version would be similar, the main thing here is the idea

2
votes

I created a UITableViewCell category/extension that allows you to turn on and off this transparency "feature".

You can find KeepBackgroundCell on GitHub

Install it via CocoaPods by adding the following line to your Podfile:

pod 'KeepBackgroundCell'

Usage:

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

Having read through all the existing answers, came up with an elegant solution using Swift by only subclassing UITableViewCell.

extension UIView {
    func iterateSubViews(block: ((view: UIView) -> Void)) {
        for subview in self.subviews {
            block(view: subview)
            subview.iterateSubViews(block)
        }
    }
}

class CustomTableViewCell: UITableViewCell {
   var keepSubViewsBackgroundColorOnSelection = false

    override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
        super.init(style: style, reuseIdentifier: reuseIdentifier)
    }

    // MARK: Overrides
    override func setSelected(selected: Bool, animated: Bool) {
        if self.keepSubViewsBackgroundColorOnSelection {
            var bgColors = [UIView: UIColor]()
            self.contentView.iterateSubViews() { (view) in

                guard let bgColor = view.backgroundColor else {
                    return
                }

                bgColors[view] = bgColor
            }

            super.setSelected(selected, animated: animated)

            for (view, backgroundColor) in bgColors {
                view.backgroundColor = backgroundColor
            }
        } else {
            super.setSelected(selected, animated: animated)
        }
    }

    override func setHighlighted(highlighted: Bool, animated: Bool) {
        if self.keepSubViewsBackgroundColorOnSelection {
            var bgColors = [UIView: UIColor]()
            self.contentView.iterateSubViews() { (view) in
                guard let bgColor = view.backgroundColor else {
                    return
                }

                bgColors[view] = bgColor
            }

            super.setHighlighted(highlighted, animated: animated)

            for (view, backgroundColor) in bgColors {
                view.backgroundColor = backgroundColor
            }
        } else {
            super.setHighlighted(highlighted, animated: animated)
        }
    }
}
0
votes

All we need is to override the setSelected method and change the selectedBackgroundView for the tableViewCell in the custom tableViewCell class.

We need to add the backgroundview for the tableViewCell in cellForRowAtIndexPath method.

lCell.selectedBackgroundView = [[UIView alloc] init];

Next I have overridden the setSelected method as mentioned below.

- (void)setSelected:(BOOL)selected animated:(BOOL)animated {
[super setSelected:selected animated:animated];

// Configure the view for the selected state

UIImageView *lBalloonView = [self viewWithTag:102];
[lBalloonView setBackgroundColor:[[UIColor hs_globalTint] colorWithAlphaComponent:0.2]];

UITextView *lMessageTextView = [self viewWithTag:103];
lMessageTextView.backgroundColor    = [UIColor clearColor];

UILabel *lTimeLabel = [self viewWithTag:104];
lTimeLabel.backgroundColor  = [UIColor clearColor];

}

Also one of the most important point to be noted is to change the tableViewCell selection style. It should not be UITableViewCellSelectionStyleNone.

lTableViewCell.selectionStyle = UITableViewCellSelectionStyleGray;

enter image description here