Thursday, July 22, 2010

Custom tintColor for each segment of UISegmentedControl

The UISegmentedControl offers the ability to set the tint color for the entire control, but it does not offer the ability to set the tint color for an individual segment. In fact, the UISegment class, the actual class for the individual segments, is not publicly documented. So, what I propose here does go a bit off the farm, but it only uses publicly available API to accomplish it. Here is the end result.
The first step is to create an extension to the UISegmentedControl. The extension neatly wraps up the new functionality so that it can be easily reused instead of cluttering up your view controllers.

UISegmentedControlExtension.h
@interface UISegmentedControl(CustomTintExtension)
-(void)setTag:(NSInteger)tag forSegmentAtIndex:(NSUInteger)segment;
-(void)setTintColor:(UIColor*)color forTag:(NSInteger)aTag;
-(void)setTextColor:(UIColor*)color forTag:(NSInteger)aTag;
-(void)setShadowColor:(UIColor*)color forTag:(NSInteger)aTag;
@end

UISegmentedControlExtension.m
#import "UISegmentedControlExtension.h"


@implementation UISegmentedControl(CustomTintExtension)

-(void)setTag:(NSInteger)tag forSegmentAtIndex:(NSUInteger)segment {
[[[self subviews] objectAtIndex:segment] setTag:tag];
}

-(void)setTintColor:(UIColor*)color forTag:(NSInteger)aTag {
// must operate by tags. Subview index is unreliable
UIView *segment = [self viewWithTag:aTag];
SEL tint = @selector(setTintColor:);

// UISegment is an undocumented class, so tread carefully
// if the segment exists and if it responds to the setTintColor message
if (segment && ([segment respondsToSelector:tint])) {
[segment performSelector:tint withObject:color];
}
}

-(void)setTextColor:(UIColor*)color forTag:(NSInteger)aTag {
UIView *segment = [self viewWithTag:aTag];
for (UIView *view in segment.subviews) {
SEL text = @selector(setTextColor:);

// if the sub view exists and if it responds to the setTextColor message
if (view && ([view respondsToSelector:text])) {
[view performSelector:text withObject:color];
}
}
}

-(void)setShadowColor:(UIColor*)color forTag:(NSInteger)aTag {

// you probably know the drill by now
// you could also combine setShadowColor and setTextColor
UIView *segment = [self viewWithTag:aTag];
for (UIView *view in segment.subviews) {
SEL shadowColor = @selector(setShadowColor:);
if (view && ([view respondsToSelector:shadowColor])) {
[view performSelector:shadowColor withObject:color];
}
}
}

@end
Once that is in place, here is an example of a typical UIViewController taking advantage of it.

SegmentColorsViewController.m

#import "SegmentColorsViewController.h"
#import "UISegmentedControlExtension.h"

#define kTagFirst 111
#define kTagSecond 112
#define kTagThird 113

@interface SegmentColorsViewController(PrivateMethods)
-(void)segmentChanged:(id)sender;
-(void)setTextColorsForSegmentedControl:(UISegmentedControl*)segmented;
@end

@implementation SegmentColorsViewController

-(void)viewDidLoad {

// create a simple segmented control
// could have done this in Interface Builder just the same
NSArray *items = [[NSArray alloc] initWithObjects:@"orange", @"yellow", @"green", nil];
UISegmentedControl *colors = [[UISegmentedControl alloc] initWithItems:items];
[items release];
[colors setSegmentedControlStyle:UISegmentedControlStyleBar];
[colors setTintColor:[UIColor lightGrayColor]];
[colors setFrame:CGRectMake(20.0f, 20.0f, 280.0f, 30.0f)];
[colors addTarget:self action:@selector(segmentChanged:) forControlEvents:UIControlEventValueChanged];
[self.view addSubview:colors];


// ... now to the interesting bits

// at some point later, the segment indexes change, so
// must set tags on the segments before they render
[colors setTag:kTagFirst forSegmentAtIndex:0];
[colors setTag:kTagSecond forSegmentAtIndex:1];
[colors setTag:kTagThird forSegmentAtIndex:2];

[colors setTintColor:[UIColor orangeColor] forTag:kTagFirst];
[colors setTintColor:[UIColor yellowColor] forTag:kTagSecond];
[colors setTintColor:[UIColor greenColor] forTag:kTagThird];

[self setTextColorsForSegmentedControl:colors];
[colors release];
}

-(void)segmentChanged:(id)sender {
// when a segment is selected, it resets the text colors
// so set them back
[self setTextColorsForSegmentedControl:(UISegmentedControl*)sender];
}

-(void)setTextColorsForSegmentedControl:(UISegmentedControl*)segmented {
[segmented setTextColor:[UIColor yellowColor] forTag:kTagFirst];
[segmented setTextColor:[UIColor blackColor] forTag:kTagSecond];
[segmented setTextColor:[UIColor blueColor] forTag:kTagThird];

[segmented setShadowColor:[UIColor redColor] forTag:kTagFirst];
[segmented setShadowColor:[UIColor whiteColor] forTag:kTagSecond];
[segmented setShadowColor:[UIColor clearColor] forTag:kTagThird];
}
@end



22 comments:

Ashok Yalamanchili said...

Nice post. Very useful for endless customer demands.

Anonymous said...

> -(void)setTintColor:(UIColor*)color forTag:(NSInteger)aTag;
> -(void)setTintColor:(UIColor*)color forTag:(NSInteger)aTag;

Huh? Twice?

*PLEASE* post only actual, working code.

shyam said...

[[[segcontrol subviews] objectAtIndex:0] setTintColor:[UIColor blackColor]];

just write this for all segment nothing to do more..

Thanks,
Shaym Parmar

Anonymous said...

shyam, this is the simple answer I've been looking for! I wasn't happy with the contrast with the default segmented control in bar mode and this is is a quick simple fix.

Anonymous said...

The comment by anonymous seems to indicate they didn't realize one method was SetText and the other was SetTint. :)

Terry said...

Daniel,
This has to be one of the most elegant, well done modifications of a standard UI control that I've seen. Kudos for a coming up with a relatively bulletproof way to modify the default behavior.

Terry

Daveice said...

Hi Daniel, really a great job.. but I found I can can either change tint color or text color, but not both, any idea?

Anonymous said...

Hello !
Very nice tutorial !

One question ?
You say that it is just using public api right ?
So it means that my application would not be rejected from the appstore because of that ?

Thomas said...

In iOS 5 beta (test beta 4 - 6) the there is a glitch where the colors are not always on the correct tab
Example:
2 tabs
Tab A and Tab B
When A is selected A is suppose to be red and B is suppose to be black
When B is selected A is suppose to be black and B is suppose to be blue

When the view is loaded and tab A is originally selected it is red and B is black (which is correct). You can switch between them and the colors are always correct. If you leave the view and tab A was selected when you leave. The colors will still be correct next time you come back.

However if select tab B and turn it Blue and tab A is black. Then you leave the view. When the view is reloaded and it re-selects B the colors will be wrong. The correct colors should be Tab A black Tab B Blue. What happens is tab A is Blue and B is black. When you tap on A to select it A turns black and B turns Red (at this point A should be red)
If you leave the view and have tab A selected (even though it is the wrong color). Next time you load the view the correct colors are loaded (A Red, B black)

Any suggestions on a fix for this?
Note this only happens in iOS 5
iOS 4.3 and lower it works perfectly

I originally thought it was a glitch with iOS 5 but as each new version of the beta goes by I have a feeling this will be broken for everyone when iOS 5 comes out

Thank You

Mark said...

After trying every other possible solution, I have found this one to be the most useable. Thanks for the "tags-not-subviews" trick. That's where the magic happens.

Anonymous said...

I liked shyam parmar's solution. It worked like a champ with only one line needed per segment, no additional .h and .m files.

[[[segcontrol subviews] objectAtIndex:0] setTintColor:[UIColor blackColor]];

Gianluca Maria Meroni said...

nice way to tint the single segments, i was looking for something like that. But now i wonder if it's a "legal" way...


it seems you are using the "private" property "tintColor" of the single elements in the UISegmentedControl, not officially declared by apple (it's declared just the property "tintColor" of the whole UISegmentedControl, then apple use it to colorize in 2 different way the elements, the selected one and the other).

so, your method could really work, and i'm considering to use it... but apple could reject your app if it's really considered a private setter method...

but i really would be glad to be wrong, of course

has any of you ever used it in an app approved for iStore?

thanks anyway,
luca

Anonymous said...

What is CustonTinExtension?
i not found cz segmentcontrol .h and .m file not sepreted in my app.
In ViewController .h file in add segment controll progra matically.

But this code give me error.

plz help me i m new in iPhone Development.

Anonymous said...


i m getting error in this code...
and what is CustomeTinExtension?
i not found cz segment controll file not sepreted in my App.
in ViewController file in add segment controll programatically.

Unknown said...

This is not guaranteed to work in future OS versions. The problem is that the setTag method assumes that the order of objects in the view array will match that of the segment indexes.

While you can get away with that prior to iOS 6, sure enough, iOS 6 breaks that assumption.

A better solution is to set the tag based on the text of the segment title which is assured to be tied to a specific segment index.

Once you've found the object (label) that contains the passed in string, you know you are looking at the correct segment object.

Anonymous said...

This might be a good alternative...

https://goddess-gate.com/dc2/index.php/post/454

Unknown said...

It's a great modification. Thanks

Anonymous said...

Very nice code!!!

I would just like to mention that some warnings popped up when using Xcode 4.6.1 and ARC and they can be suppressed with pragmas:

#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
[view performSelector:text withObject:color];
#pragma clang diagnostic pop

Pete said...

Ran into a problem in which the order of the subviews did not match the order of the segments. In particular, the subviews were in reverse order to the segment order. The following code deals with this:


-(UIView*)viewForSegment:(NSUInteger)segmentIndex
{
NSArray* subviews = [self subviews];

//Sort the views from leftmost to rightmost
NSArray* sortedViews = [subviews sortedArrayUsingComparator:^NSComparisonResult(id obj1, id obj2) {
UIView* view1 = obj1;
UIView* view2 = obj2;

NSComparisonResult ret;

if (view1.frame.origin.x < view2.frame.origin.x) {
ret = NSOrderedAscending;
} else if (view1.frame.origin.x > view2.frame.origin.x) {
ret = NSOrderedDescending;
} else {
ret = NSOrderedSame;
}

return ret;
}];

//The view we're really interested in is...
return sortedViews[segmentIndex];
}

-(void)setTag:(NSInteger)tag forSegmentAtIndex:(NSUInteger)segment {
#if 1
//Old code: Index by segment index
[[self viewForSegment:segment] setTag:tag];
#else
//Old code: Index by subview index
[[[self subviews] objectAtIndex:segment] setTag:tag];
#endif
}

StDogbert said...

Here is my implementation of automatic tags setter method in swift:

extension UISegmentedControl {
func setTags() {
var tab_items: [String] = []

for i in 0 ... (self.numberOfSegments - 1) {
tab_items.append(self.titleForSegmentAtIndex(i) ?? "")
}

for segment in self.subviews {
for segment_subview in segment.subviews {
let selector = Selector("text")
if segment_subview.respondsToSelector(selector) {
if let text = segment_subview.performSelector(selector).takeRetainedValue() as? String {
for item in tab_items {
if text == NSLocalizedString(item, comment: "") {
if let index = tab_items.indexOf(item) {
segment.tag = index
}
}
}
}
}
}
}
}
}

U ll still need to call setTags() manually so.
Also as u can c this method doesn't guarantee u that tags will be set. Anyway, most likely they will.

StDogbert said...

Also, just in case anyone needs this here is a method to set tint colors in swift

func setTintColor(color color: UIColor?, tag: Int) {
if let segment = self.viewWithTag(tag) {
let selector = Selector("setTintColor:")
if segment.respondsToSelector(selector) {
segment.performSelector(selector, withObject: nil)
segment.performSelector(selector, withObject: color)
}
}

}

Just add it to the extension above.

While I was writing this method found a bug in one I posted above:
takeRetainedValue() should be takeUnretainedValue(). Obviously.

charlesr said...

This is great. Just what I am looking for in order to set the background colour scheme in my app settings UI. And thanks to 'Anonymous' for the pragma advice...