May 27, 2010

iPhone: UISegmentedControl with custom colors

[UPDATE]: We had an app approved with this code. Keep in mind that Apple has very inconsistent policies regarding app approval, but it's looking good for now.

Tasked with building a UISegmentedControl with different colors for selected/unselected buttons, I created a subclass that accomplishes this by digging through the subviews of the segmented control. I can't verify that this will get approved by the mysterious app store process, nor can I say it will function properly if you're using all of the built-in functions of UISegmentedControl (adding/removing segments dynamically will break this code). But for a simple case, it's working well for my purposes. Here we go:

CustomSegmentedControl.h :
#import 


@interface CustomSegmentedControl : UISegmentedControl {
 
 UIColor *offColor;
 UIColor *onColor;
 
 BOOL hasSetSelectedIndexOnce;
}

-(id)initWithItems:(NSArray *)items offColor:(UIColor*)offcolor onColor:(UIColor*)oncolor;
-(void)setInitialMode;
-(void)setToggleHiliteColors;

@end

CustomSegmentedControl.m :
#import "CustomSegmentedControl.h"


@implementation CustomSegmentedControl


-(id)initWithItems:(NSArray *)items offColor:(UIColor*)offcolor onColor:(UIColor*)oncolor {
 if (self = [super initWithItems:items]) {
        // Initialization code
  offColor = [offcolor retain];
  onColor = [oncolor retain];
  hasSetSelectedIndexOnce = NO;
  [self setInitialMode];
  [self setSelectedSegmentIndex:0];  // default to first button, or the coloring gets all whacked out :(
    }
    return self;
}

-(void)setInitialMode
{
 // set essential properties
 [self setBackgroundColor:[UIColor clearColor]];
 [self setSegmentedControlStyle:UISegmentedControlStyleBar];
 
 // loop through children and set initial tint
 for( int i = 0; i < [self.subviews count]; i++ )
 {
  [[self.subviews objectAtIndex:i] setTintColor:nil];
  [[self.subviews objectAtIndex:i] setTintColor:offColor];
 }
 
 // listen for updates
 [self addTarget:self action:@selector(setToggleHiliteColors) forControlEvents:UIControlEventValueChanged];
}

-(void)setToggleHiliteColors
{
 // get current toggle nav index
 int index = self.selectedSegmentIndex;
 int numSegments = [self.subviews count];
 
 for( int i = 0; i < numSegments; i++ )
 {
  // reset color
  [[self.subviews objectAtIndex:i] setTintColor:nil];
  [[self.subviews objectAtIndex:i] setTintColor:offColor];
 }
 
 if( hasSetSelectedIndexOnce )
 {
  // this is super weird - the subviews array is backwards... so deal with it like that
  [[self.subviews objectAtIndex: numSegments - 1 - index] setTintColor:onColor];
 }
 else
 {
  // ...but the very first time, they're the expected order :-/
  [[self.subviews objectAtIndex: index] setTintColor:onColor];
  hasSetSelectedIndexOnce = YES;
 }

}


@end 
And to initialize :
NSArray *toggleItems = [[NSArray alloc] initWithObjects:@"One",@"Two",@"Three",nil];
CustomSegmentedControl *toggleNav = [[CustomSegmentedControl alloc] initWithItems:toggleItems offColor:[UIColor blackColor] onColor:[UIColor redColor] ];
[toggleNav addTarget:self action:@selector(handleToggleNav:) forControlEvents:UIControlEventValueChanged];
[toggleNav setFrame:CGRectMake(52, 8, 211, 25)]; 
[self.view addSubview:toggleNav];
[toggleNav release];

Otherwise, follow the documentation for a UISegmentedControl, and enjoy.

13 comments:

  1. Coolness! It's working :) Thanks a lot! You're the best! You've saved me a damn headache!

    I also tried this one here: http://matteocaldari.it/2010/05/a-uisegmentedcontrol-with-custom-color but couldn't get it working!

    It might be worth a try too tho ;)

    ReplyDelete
  2. Hi, great piece of code :)
    Did you get an App approved with it?

    ReplyDelete
  3. Update: we did have an app approved with this code :)

    ReplyDelete
  4. Thanks a ton..!

    It worked like a charm :)

    ReplyDelete
  5. Why do I get thsi error: "modifying layer that is being finalized"
    and every time I click on a button my App closes itself?

    I am using the same code as in your post :(

    ReplyDelete
  6. hmm, I'm not sure. I've never seen that error before. I imagine it could be a matter of where you're implementing the code. Are you using the code in the same place that you're building other elements?

    ReplyDelete
  7. I was getting a memory leak until I added this method:

    - (void)dealloc {
    [offColor release];
    offColor = nil;
    [onColor release];
    onColor = nil;

    [super dealloc];
    }

    ReplyDelete
  8. i get 2 selected button if i put this in code
    [segmentedButton setSelectedSegmentedIndex:1];
    Need Help urgent

    ReplyDelete
  9. hi justin...
    it's a nice tutorial...
    thx for sharing

    but i want to ask something, why i can't change the offcolor and oncolor with [UIImage colorWithPatternImage:...]??
    is there any way to do that with this tutorial???

    ReplyDelete
  10. Lie, rZz:

    Sorry guys, it was a hack to make this work for my specific situation. Like I said in the blog post, not everything works that normally would in a UISegmentedControl, including setSelectedSegmentedIndex :(

    Unfortunately, Apple keeps their UI components very locked down.

    ReplyDelete
  11. hi justin,

    i added that
    [toggleNav setMomentary:YES];
    to make buttons clickable although it is selected. but first time just after load, the first button looks like selected. and then if i click then first button it does not response. if i click the first button second time it works fine. if i click the second button instead of the first button it works fine too. do you have any idea about this bug (seems like) and any solution?

    thanks.

    ReplyDelete
  12. I noticed you had this hasSetSelectedIndexOnce because you said the first time they are in expected order and then they turn backwards. You don't need this. The segments are in order when they have not been added to the window, after they are added then they are backwards. So your toggle code can be changed from
    if( hasSetSelectedIndexOnce )
    to
    if( self.window )

    Also make sure to wrap that whole block in if(index != UISegmentedControlNoSegment){...}
    otherwise it will crash if you remove the selection.

    ReplyDelete
  13. Hi,

    i have implemented the above code but my view segmentedcontrol does not change color when a segment is being selected i have put off color to red and on color to cyan but its not working can you please help me . I want to change color of selected segment only.

    Pls help.

    Thanks,

    ReplyDelete