5

I have a standard UITableView. I want to set the shadowColor of the cell's textLabel to [UIColor whiteColor], but only when the cell is touched. For that, I'm using the following code. It's a custom UITableViewCell subclass that overrides setSelected/setHighlighted:

@implementation ExampleTableViewCell


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

}

- (void)setHighlighted:(BOOL)highlighted animated:(BOOL)animated {
    [super setHighlighted:highlighted animated:animated];
    [self setShadowColorSelected:highlighted];
}

- (void)setShadowColorSelected:(BOOL)selected {
    if (selected) {
        self.textLabel.shadowColor = [UIColor blackColor];
    }else {
        self.textLabel.shadowColor = [UIColor whiteColor];
    }
}

@end

My problem with this approach is that, on deselection, the cell has a very short period where both the label's text and shadow are white. See this screenshot, which was taken in the exact moment of deselection:

Shadow Example

It's basically the same approach as in these two posts:

UILabel shadow from custom cell selected color

Removing text shadow in UITableViewCell when it's selected

I'm using the approach of the accepted answer in the latter question.

I have created a very very simple code project and uploaded it to github. It shows off my problem. It's just a UITableViewController that displays a single cell.

Apart from that, there's nothing fancy. UITableView delegate methods:

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    static NSString *CellIdentifier = @"Cell";
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
    if (!cell) {
        cell = [[ExampleTableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier];
    }
    cell.textLabel.text = @"test";

    return cell;
}

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
    [self.tableView deselectRowAtIndexPath:indexPath animated:YES]; //setting this to NO doesn't work either!
}

Any ideas?

Community
  • 1
  • 1
avf
  • 858
  • 11
  • 24
  • I'm just thinking out loud here, but have you tried maybe setting a condition in either `setSelected` or `setHighlighted` (one of them) so that `setShadowColorSelected` is only called when the `BOOL` value is `true`? It's possible that one of the two methods are being called before the other which is why the shadow turns white before the highlight is removed. – sooper Oct 24 '12 at 19:21
  • Well I tried something similar, which is implementing only one of the methods, so either `setSelected` OR `setHighlighted`. Results in the same issue. – avf Oct 24 '12 at 19:45
  • I can't get what is the problem here, the behavior seems to be correct for me. You don't expect `setHighlighted:` or `setSelected:` to block the thread until the animation is finished, do you ? – A-Live Oct 24 '12 at 20:23
  • No, I don't. The problem is, that in the above screenshot, the `shadowColor` is white, (which means that setSelected/setHighlighted has been called with NO on the cell), but the `UILabel's` `textColor` is also white, which is its `highlightedTextColor`. So the label is still in the 'highlighted' state, but the cell is not. – avf Oct 24 '12 at 20:26
  • Since you know that your methods are being called with `NO`, then maybe you could try setting `self.textLabel.textColor` to white/black inside your `setShadowColorSelected` method as well? – sooper Oct 24 '12 at 23:04
  • Yeah, I tried that as well. It kind of works, but then the problem shifts: the labels change their color BEFORE the background, which also looks very odd. It seems, the tableViewCell updates its stuff quite some time *after* calling `setSelected`. (Even with the animation parameter set to NO). – avf Oct 24 '12 at 23:53
  • I can clearly see the problem in the screenshot, but I get no such problem using your example code in an iOS 5 test app. Unless the change only lasts a single frame or something, and I've missed it. Odd. – Metabble Oct 30 '12 at 18:03
  • @Metabble Yes, it's just on one single frame! But it looks really bad, especially when using different colors and the contrast is stronger. – avf Oct 30 '12 at 18:32

8 Answers8

5

If I understood the problem, you need to display the shadow color until the cell selection is animated to be gone. I'm not sure what is incorrect in the way you tried, more straightforward solution works fine though.

Please note, you'll need to remove observer once it's not needed.

ExampleTableViewCell.h

@interface ExampleTableViewCell : UITableViewCell {

}

- (void) setSelectionShadowOfColor:(UIColor *) selColor;
@end

ExampleTableViewCell.m

@implementation ExampleTableViewCell

- (void) setSelectionShadowOfColor:(UIColor *) selColor {
    self.textLabel
    [self addObserver:self
           forKeyPath:@"textLabel.highlighted" // not isHighlighted as that is a getter name of the highlighted property
              options:NSKeyValueObservingOptionNew
              context:NULL];
}


- (void)observeValueForKeyPath:(NSString *)keyPath
                      ofObject:(id)object
                        change:(NSDictionary *)change
                       context:(void *)context {

    BOOL isHighlighted = [[change objectForKey:NSKeyValueChangeNewKey] boolValue];

    if (isHighlighted) {
        self.textLabel.shadowColor = [UIColor blackColor];
    } else {
        self.textLabel.shadowColor = [UIColor whiteColor];
    }
}

@end

ExampleTableViewController.m

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    static NSString *CellIdentifier = @"Cell";
    ExampleTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier]; // note the type ExampleTableViewCell is used here to avoid the interface lookup mess
    if (!cell) {
        cell = [[ExampleTableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier];
        [cell setSelectionShadowOfColor:[UIColor blackColor]];
    }
    cell.textLabel.text = @"test";

    return cell;
}
A-Live
  • 8,904
  • 2
  • 39
  • 74
  • Yes! That's it. I thought about KVO-ing the `highlighted` property of the label, but I thought it was too much of a hack. But it's actually the only answer that works until now. I'm gonna wait a bit and see if someone comes up with something even better until I accept your answer and ask Allen to award the bounty to you. – avf Oct 31 '12 at 17:36
  • I totally agree with you, if any better (or other working) solution comes here, please let me know via @A-Live at the comment. – A-Live Nov 01 '12 at 11:23
  • How would you do this in Swift 2.2? Is there a similar way? – blackjacx Jul 01 '16 at 22:26
  • I can't see any issue of using the same approach in swift, do you have any particular issue, @blackjacx ? – A-Live Jul 04 '16 at 10:11
  • Yes sorry it is possible in Swift. Sorry I read about that after my question ;-) – blackjacx Jul 04 '16 at 15:38
2

Add the below to your UITableViewDelegate:

NSIndexPath *selectedIndex;

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    static NSString *CellIdentifier = @"Cell";
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
    if (!cell) {
        cell = [[ExampleTableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier];
    }
    cell.textLabel.text = @"test";
    if(indexpath == selectedIndex)
    {
        cell.textlabel.shadowColor = [UIColor blackColor];
    }
    else
    {
        cell.textlabel.shadowColor = [UIColor whiteColor];
    }
    return cell;
}

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
    [self.tableView beginUpdates];
    selectedIndex = indexpath;
    [self.tableView endUpdates];
}
MuhammadBassio
  • 1,590
  • 10
  • 13
  • Sorry, but how is this supposed to work? `cellForRowAtIndexPath` is not even called on selection. – avf Oct 31 '12 at 17:32
  • sorry, I forgot to update the `UITableView`, check the edited answer, it should now work :) – MuhammadBassio Nov 01 '12 at 10:34
  • Ok, yeah, this might actually work, but I don't wanna update the whole tableView on every selection. – avf Nov 01 '12 at 16:50
0

This is the best I was able to do:

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

    [self setShadowColorSelected:selected];
}

- (void)setHighlighted:(BOOL)highlighted animated:(BOOL)animated {
    [super setHighlighted:highlighted animated:YES];

    [self setShadowColorSelected:highlighted];
}

- (void)setShadowColorSelected:(BOOL)selected {
    [self.textLabel setBackgroundColor:[UIColor clearColor]];

    if (selected)
    {
        [self.textLabel setTextColor:[UIColor whiteColor]];
        [self.textLabel setShadowColor:[UIColor blackColor]];

        [UIView setAnimationBeginsFromCurrentState:YES];
        [UIView transitionWithView:self.textLabel duration:0.25 options:UIViewAnimationOptionTransitionCrossDissolve animations:^{
            if (selected)
            {
                [self.textLabel setTextColor:[UIColor whiteColor]];
                [self.textLabel setShadowColor:[UIColor blackColor]];
            }
            else
            {
                [self.textLabel setTextColor:[UIColor blackColor]];
                [self.textLabel setShadowColor:[UIColor whiteColor]];
            }
        } completion:nil];
    }
    else
    {
         [self.textLabel setTextColor:[UIColor blackColor]];
         [self.textLabel setShadowColor:[UIColor whiteColor]];
    }
}
Rick Pastoor
  • 3,625
  • 1
  • 21
  • 24
  • Thanks, your answer kind of works, however it doesn't look good on non-animated deselection (when calling `[self.tableView deselectRowAtIndexPath:indexPath animated:NO];`). There must be a better way to solve this, it's such a common thing to do. – avf Oct 30 '12 at 21:42
0

According to my understanding you can eliminate this issue by changing the color of the tableview by using following code with the color needed

[tableView setBackgroundColor:[UIColor "GrayColor"]];
Mick MacCallum
  • 129,200
  • 40
  • 280
  • 281
Teja Swaroop
  • 1,139
  • 12
  • 18
0

Here is a simple way to accomplish what you want:

-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {

    self.textLabel.shadowColor = [UIColor blackColor];
    [super touchesBegan:touches withEvent:event];
}
-(void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {

    self.textLabel.shadowColor = [UIColor whiteColor];
    [super touchesEnded:touches withEvent:event];
}
-(void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event {

    self.textLabel.shadowColor = [UIColor whiteColor];
    [super touchesCancelled:touches withEvent:event];
}
brynbodayle
  • 6,546
  • 2
  • 33
  • 49
  • Thanks, I tried that, but unfortunately it doesn't solve the problem. Also, it's not very elegant to just override the event handling code for this and it might cause problems in some cases. Where/in which method would you set the shadow back to whiteColor with this approach? With the code you posted, it would just stay black even when deselection is done. – avf Oct 30 '12 at 21:40
  • I had the colors inverted from what you wanted. Fixed that now. If you try out the code you will see that it works. When touchesEnded or touchesCancelled is called, the shadowColor is set back to the original color. – brynbodayle Oct 30 '12 at 21:56
  • Just tried it, doesn't work. Gives the exact same result, I can produce the same screenshot as above with your code :( Look closely, it's only one frame when you let go of the cell. – avf Oct 30 '12 at 22:06
0

I guess you should need to animate the changing of the text/shadow color with the same duration that UITableView uses to animate selection/deselection. In my understanding, you change the text/shadow color at the exact moment that the tableView begins animating the (dis)appearing of the selection highlight, so what you get is that your colors change momentarily, while the selection highlight takes some time to animate from one state to another

Try something like this:

__block UIColor *newShadowColor = selected ? [UIColor blackColor] : [UIColor whiteColor];
[UIView animateWithDuration:0.2
                 animations:^{
                        /* change your label/shadow color here. */
                        self.textLabel.shadowColor = newShadowColor;
                 }
                 completion:^(BOOL finished){
                       /* this is where the cell is no longer selected
                          or highlighted. You may do some additional style changes to your
                          label here */ }];
kervich
  • 487
  • 3
  • 13
  • Like the other answer by Rick Pastoor, doesn't work on non-animated deselection. – avf Oct 31 '12 at 17:33
  • Well, simply set the animationDuration to 0 if animated==NO in -setSelected:animated: – kervich Nov 01 '12 at 07:24
  • If I set the animationDuration to 0, how does this differ from my code? I'd be able to produce the same screenshot, wouldn't I? – avf Nov 01 '12 at 16:52
0

I had the same issue. All solutions I looked at required subcassing / too much additional code.

What I did in the end is to create a second UILabel underneath the primary UILabel to act as a shadow.

Don't set shadows on your primary and shadow labels. For the shadow label, set the 'Normal Color' to what you wanted your shadow color to be and set the highlighted color to 'Clear Color'.

Obviously you have to update the shadow label each time you update the primary label. Not a big price to pay in many cases.

Hope that helps!

Nikolay Suvandzhiev
  • 8,465
  • 6
  • 41
  • 47
  • Hmm, yeah, I can see that this could work, but it seems like really a lot of work for such a simple thing. I like A-Live's KVO-solution better. – avf Oct 31 '12 at 17:37
0

I was also facing the same problem.

Instead of using default label, you can use UIButton and your problem will be solved.

Put custom button in cell.

My requirements were solved. It might help you.

Hemang
  • 1,224
  • 9
  • 15