Flickring UIButton State Issue

Ran into a very specific "bug" in my iPhone application today. We have on our about page of the application two images for the highlighted and normal states of a button. It works as expected when you "press" and then "touch up" at a slow pace, but if you click/tap it quickly, there's a noticeable flicker between states. Here's the code inside the subclass of UIButton that creates the buttons:

UIImage *normalImage = [[UIImage imageNamed:@"btn-small.png"] stretchableImageWithLeftCapWidth:10.0f topCapHeight:0.0f];
UIImage *highlightedImage = [[UIImage imageNamed:@"btn-small-down.png"] stretchableImageWithLeftCapWidth:10.0f topCapHeight:0.0f];

[self setBackgroundImage:normalImage forState:UIControlStateNormal];
[self setBackgroundImage:highlightedImage forState:UIControlStateDisabled];
[self setBackgroundImage:highlightedImage forState:UIControlStateHighlighted];

[self setAdjustsImageWhenDisabled:FALSE];
[self setAdjustsImageWhenHighlighted:FALSE];

When a button is tapped it simply disables itself and enables the other button:

- (IBAction)aboutButtonTouched:(id)sender
{
    aboutButton.enabled = FALSE;
    rulesButton.enabled = TRUE;
}

- (IBAction)rulesButtonTouched:(id)sender
{
    rulesButton.enabled = FALSE;
    aboutButton.enabled = TRUE;
}

This works fine, but there's a noticable "flicker" when tapping the button quickly (and not a delicate press, hold, and release). The solution to this flickering took a bit of reverse engineering. The first thing I did was modify the aboutButtonTouched method to log the button's state property which is a bit-mask NSUInteger:

- (IBAction)aboutButtonTouched:(id)sender
{
    rulesButton.enabled = TRUE;
    aboutButton.enabled = FALSE;    
    
    NSLog(@"%d", [sender state]);
}

At this point, the button is disabled through setEnabled, and the log reported that the state was "3". Looking at the bit-mask type for UIControlState (Comments added since I can never remember bitwise):

enum {
   UIControlStateNormal               = 0,            // 0
   UIControlStateHighlighted          = 1 << 0,       // 1
   UIControlStateDisabled             = 1 << 1,       // 2
   UIControlStateSelected             = 1 << 2,       // 4
   UIControlStateApplication          = 0x00FF0000,
   UIControlStateReserved             = 0xFF000000
};

We can see that to get "3" (0011) we should use UIControlStateHighlighted | UIControlStateDisabled (0001|0010 or 1|2), something which I did not have as a state in my original button definition. The key here that there's a brief time when the state is both before only being disabled ("A control enters this state when a touch enters and exits during tracking and and when there is a touch up" -- from the docs). So the final state settings for the button where it does not flicker are:

[self setBackgroundImage:normalImage forState:UIControlStateNormal];
[self setBackgroundImage:highlightedImage forState:UIControlStateDisabled];
[self setBackgroundImage:highlightedImage forState:UIControlStateHighlighted];
[self setBackgroundImage:highlightedImage forState:UIControlStateHighlighted|UIControlStateDisabled];
April 4th, 2010 | Permalink