a color indicator in a tableView cell

Today I created a static tableview which should allow the user to change colors of items.

To indicate the selected color I put a UIImageView (to display a horizontal red bar if no color is used) in the cell, and set its backgroundColor property.

Because this is a static tableView I set the backgroundColor of my color indicator views in a method called - configureView. Nothing fancy.

- (void)configureView {
    for (UIImageView *colorView in self.colorViews) {
        colorView.backgroundColor = [self colorForView:colorView];
    }
}

After I implemented everything I took it for a test ride.

tableView before pushing

Works as intended.

If you select the row a segue will push a color picker view controller, where you can select a new color.

color selection viewController

So I set the color and tapped the back button

tableView during popping

Everything is fine.

But moments later…

tableView after popping

The color is reset to the value before we changed it.

Let’s debug it

So I started to check what I did wrong. Maybe I didn’t set the selected color. Nope, I did that. I used the debugger to step over each line of code that happened after I pressed the back button.

But I couldn’t figure out what I did wrong.

To figure out which method changed the backgroundColor I subclassed the color indicator view and changed - setBackgroundColor: to print the stack trace.

- (void)setBackgroundColor:(UIColor *)backgroundColor {
    [super setBackgroundColor:backgroundColor];
    NSLog(@"%@", [NSThread callStackSymbols]);
}

I went back to the color picker and cleared the log.

The first stack trace looked good. Exactly like it should. The view will appear, and configureView changes the backgroundColor

0  MyApp  0x00034ddf -[MBImageView setBackgroundColor:] + 143
1  MyApp  0x00027bef -[MBColorListTableViewController configureView] + 639
2  MyApp  0x00027303 -[MBColorListTableViewController viewWillAppear:] + 115

But. The backgroundColor property is changed more often…

0  MyApp  0x00034ddf -[MBImageView setBackgroundColor:] + 143
1  UIKit  0x0053bd92 -[UITableViewCell _setOpaque:forSubview:] + 896
2  UIKit  0x0053bed9 -[UITableViewCell _setOpaque:forSubview:] + 1223
3  UIKit  0x0053bed9 -[UITableViewCell _setOpaque:forSubview:] + 1223
4  UIKit  0x0053cfae -[UITableViewCell _updateHighlightColors] + 198
5  UIKit  0x0053cb43 -[UITableViewCell _deselectAnimationFinished] + 178
6  UIKit  0x00387d66 -[UIViewAnimationState sendDelegateAnimationDidStop:finished:] + 237
7  UIKit  0x00387f04 -[UIViewAnimationState animationDidStop:finished:] + 68

There it is. After I changed the backgroundColor, the cell changed the backgroundColor too.

And it appears it changes the color back to the previous value.

And if I hadn’t cleared the log without looking at it, I would have seen that the same thing happened before

0  MyApp  0x00034de3 -[MBImageView setBackgroundColor:] + 115
1  UIKit  0x0053bd92 -[UITableViewCell _setOpaque:forSubview:] + 896
2  UIKit  0x0053bed9 -[UITableViewCell _setOpaque:forSubview:] + 1223
3  UIKit  0x0053bed9 -[UITableViewCell _setOpaque:forSubview:] + 1223
4  UIKit  0x0053cfae -[UITableViewCell _updateHighlightColors] + 198
5  UIKit  0x0053c8b3 -[UITableViewCell showSelectedBackgroundView:animated:] + 1455
6  UIKit  0x0053c9ed -[UITableViewCell setHighlighted:animated:] + 224

So why does this happen?

It turns out when you select a cell by tapping it, the cell goes through all its subviews and sets the backgroundColor to UIDeviceWhiteColorSpace 0 0 also known as [UIColor clearColor]. And when you deselect the cell, it goes again through all its subviews and sets the backgroundColor back to their previous (before it was selected) value.

This is what happens to the backgroundColor property:

  1. The tableView is presented
    backgroundColor -> gray

  2. I select the row, selection animation does start
    backgroundColor -> clear

  3. The next viewController is pushed, there I change the color and pop back to the table
    backgroundColor -> green

  4. The tableview appears and starts and finishes the deselection animation.
    backgroundColor -> gray

This is done so the selectedBackground is visible through all the UIViews in the cell (which for performance reasons should have a white backgroundColor, instead of a clear one).

Conclusion

Obviously you can’t change the backgroundColor of a UITableViewCell subview after the selection animation started and before the deselection animation has come to an end.

As far as I know that behavior is not documented anywhere.

Solution

You could save the proposed backgroundColor as a property on the viewController and set it after the animation is completed. You could probably kill the animation completely too.

But I went for a better (as in cleaner, easier and better looking) method. I simply added a CALayer to the UIImageView in viewDidLoad

- (void)viewDidLoad
{
    [super viewDidLoad];
    for (UIImageView *colorView in self.colorViews) {
        CALayer *colorLayer = [CALayer layer];
        colorLayer.frame = CGRectMake(0, 0, colorView.bounds.size.width, colorView.bounds.size.height);
        [colorView.layer addSublayer:colorLayer];
    }
}

And I changed my configureView method so it adjusts the backgroundColor of that layer

- (void)configureView {
    for (UIImageView *colorView in self.colorViews) {
        CALayer *colorLayer = colorView.layer.sublayers[0];
        colorLayer.backgroundColor = [self colorForView:colorView].CGColor;
    }
}

Works great.

Posted in Coding Tagged with: , ,