Customizing appearance of UISegmentedControl

You are probably familiar with the UISegmentedControl and its default look which is found in many native iOS apps:

Generic segmented control

This UI control can be created using the UISegmentedControl Class which is available in the UIKit framework. The appearance of this control can be easily customized yet doing so is not always obvious to new iOS developers. I was recently asked to create a row of separate buttons that would behave in a similar manner to UISegmentedControl - where only one segment could be selected at any given time. The design looked something like this (specifically this one I took from dribbble):

Custom segmented control

Generally speaking, the appearance of the UISegmentedControl could be customized by using the following methods that set the background and divider images:

- (void)setBackgroundImage:(UIImage *)backgroundImage
        forState:(UIControlState)state
        barMetrics:(UIBarMetrics)barMetrics;

- (void)setDividerImage:(UIImage *)dividerImage
        forLeftSegmentState:(UIControlState)leftState
        rightSegmentState:(UIControlState)rightState
        barMetrics:(UIBarMetrics)barMetrics;

The background image is used to fill stretchable parts of the control segments as well as its edges. Therefore, this image should have equal-sized left and right parts which are used for the left and right edges of the control respectively, separated by a 1 pixel wide line (2 pixels for retina images) that is used to fill the segments by stretching itself horizontally. The divider images are used to fill the space between segments. These images are used “as is” without being stretched and therefore their width defines the space between the control segments. The following diagram shows how these images are used in UISegmentedControl:

UISegmentedControl diagram 1

Now, after we know how UISegmentedControl uses these images to present itself we can write some code to make it happen. In the following example I have created a subclass of UISegmentedControl called MySegmentedControl which has all the needed code to customize the appearance of our control. Bare in mind that subclassing is not required, everything shown here could be done by sending those same messages (this is Apple’s terminology for “method invokation”) to an instance of the UISegmentedControl class.

First of all, we need to create all 5 images: 2 background images for the selected and normal (unselected) states and 3 divider images. The 3 divider images are used for:

  1. The left and right edges of the adjacent unselected segments
  2. The left edge of the unselected segment and the right edge of the adjacent selected segment
  3. The left edge of the selected segment and the right edge of the adjacent unselected segment
  • segmented-control-selected mySegCtrl-selected-bkgd.png
  • segmented-control-normal mySegCtrl-normal-bkgd.png
  • segmented-control-divider-none-selected mySegCtrl-divider-none-selected.png
  • segmented-control-divider-right-selected mySegCtrl-divider-right-selected.png
  • segmented-control-divider-left-selected mySegCtrl-divider-left-selected.png

Then we need to create a subclass of UISegmentedControl, by creating header and implementation file:

#import <UIKit/UIKit.h>

@interface MySegmentedControl : UISegmentedControl
@end

In the implementation file for MySegmentedControl we will override the designated initializer - (id)initWithItems:(NSArray *)items and call the appearance methods:

#import "MySegmentedControl.h"

@implementation MySegmentedControl

- (id)initWithItems:(NSArray *)items {
    self = [super initWithItems:items];
    if (self) {
        // Initialization code

        // Set divider images
        [self setDividerImage:[UIImage imageNamed:@"mySegCtrl-divider-none-selected.png"]
              forLeftSegmentState:UIControlStateNormal
              rightSegmentState:UIControlStateNormal
              barMetrics:UIBarMetricsDefault];
        [self setDividerImage:[UIImage imageNamed:@"mySegCtrl-divider-left-selected.png"]
              forLeftSegmentState:UIControlStateSelected
              rightSegmentState:UIControlStateNormal
              barMetrics:UIBarMetricsDefault];
        [self setDividerImage:[UIImage imageNamed:@"mySegCtrl-divider-right-selected.png"]
              forLeftSegmentState:UIControlStateNormal
              rightSegmentState:UIControlStateSelected
              barMetrics:UIBarMetricsDefault];

        // Set background images
        UIImage *normalBackgroundImage = [UIImage imageNamed:@"mySegCtrl-normal-bkgd.png"];
        [self setBackgroundImage:normalBackgroundImage
              forState:UIControlStateNormal
              barMetrics:UIBarMetricsDefault];
        UIImage *selectedBackgroundImage = [UIImage imageNamed:@"mySegCtrl-selected-bkgd.png"];
        [self setBackgroundImage:selectedBackgroundImage
              forState:UIControlStateSelected
              barMetrics:UIBarMetricsDefault];
    }
    return self;
}

@end

We are not finished just yet – if we embed the above control in a UIViewController we will see that the text inside the control’s segments is not aligned properly. The following code will produce undesired results:

NSArray *items = @[@"first", @"second", @"third"];
MySegmentedControl *mySegmentedControl = [[MySegmentedControl alloc] initWithItems:items];
mySegmentedControl.frame = CGRectMake(10, 7, self.view.frame.size.width - 20, mySegmentedControl.frame.size.height);
mySegmentedControl.selectedSegmentIndex = 0;
[self.view addSubview:mySegmentedControl];

And the result:

Segmented control unaligned text

So why is this happening? Well, the answer is hidden in the implementation of UISegmentedControl. What actually happens is that UISegmentedControl center-aligns text inside its segments according to their width, but the segment’s width doesn’t include the divider images. Here’s a diagram to clarify things:

UISegmentedControl diagram 2

To solve this issue we need to adjust the content position of the first and last segments by 1/2 the width of the divider images, assuming that all divider images have the same width. To do this we need to add two lines of code to MySegmentedControl:

[self setContentPositionAdjustment:UIOffsetMake(dividerImageWidth / 2, 0)
      forSegmentType:UISegmentedControlSegmentLeft
      barMetrics:UIBarMetricsDefault];
[self setContentPositionAdjustment:UIOffsetMake(- dividerImageWidth / 2, 0)
      forSegmentType:UISegmentedControlSegmentRight
      barMetrics:UIBarMetricsDefault];

And with that we reach our final result:

Segmented control aligned text


comments powered by Disqus