Swift iOS Tutorial: How to Customize UISegmentedControl

April 28, 2017

Customizing a UISegmentedControl

If you have spent any time developing an iOS app, you have probably realized that customizing UI elements in Swift can be hard. There is either a checkbox that does what you want, or it takes hours of research and trial and error to get something just the way you want it. For example, customizing a UISegmentedControl to display the selected segment as white is pretty straightforward. Apple provides a place to make this change in storyboard or you can change the tintColor property in code.  However, changing the selected segment to have a darker background while the text and border remain white as in the image below is not so trivial.

Custom UISegmentedControl example

This Swift iOS tutorial will help you get a segmented control that behaves as the one above.  As a disclaimer, the gradient background is just an image behind the UISegmentedControl, and the segments and their titles are set up in storyboard. This tutorial is primarily focused on customizing the look of the selected segment.

The Tutorial: CustomSegmentedControl

Add a new file to your project called CustomSegmentedControl.swift and add the following code to it:

import UIKit class CustomSegmentedControl: UISegmentedControl {     required init?(coder aDecoder: NSCoder) {        super.init(coder: aDecoder)        configure()     }     private func configure() {    }    func changeSelectedIndex(to newIndex: Int) {    }}

Identifying the Problem

The inherent problem with trying to get the selected segment’s background to change independently of everything else is that by default, the selected segment changes its background to fill with the UISegmentedControl’s tint color and changes its text color to be clear.  Any attempts to change the background color of the segment will be overwritten by this default behavior as soon as it is selected. So we need to work around this behavior.

Important: First, set up an IBAction outlet for the UISegmentedControl’s ValueChanged and make sure that sender is a CustomSegmentedControl.  In that function, call sender.changeSelectedIndex(to: sender.selectedSegmentIndex).

@IBAction func segmentControlValueChanged(_ sender: CustomSegmentedControl) {    sender.changeSelectedIndex(to: sender.selectedSegmentIndex)}

Because we cannot change the default behavior of the selected segment, the first thing we have to do is to make it so that the selected segment isn’t actually selected.  Add a currentIndex variable to manually keep track of which segment is selected.  In changeSelectedIndex, set the currentIndex to newIndex and set the UISegmentControl’s selectedSegmentIndex to UISegmentedControlNoSegment. This will deselect the selected segment and now we can begin to customize the UISegmentcontrol. This is the current state of the file:

import UIKitclass CustomSegmentedControl: UISegmentedControl {    var currentIndex: Int = 0       required init?(coder aDecoder: NSCoder) {        super.init(coder: aDecoder)        configure()    }    private func configure() {    }    func changeSelectedIndex(to newIndex: Int) {        currentIndex = newIndex        self.selectedSegmentIndex = UISegmentedControlNoSegment    }}

Solving the Problem

The only way to change properties of the views in a UISegmentedControl is to access its list of subviews. This list, however, is not guaranteed to be in any particular order.  Because of this, add a variable to the class called sortedViews that can be used to access the subviews of the UISegmentedControl.  In configure, sort the array of subviews by their x position and save it in the new variable.

import UIKitclass CustomSegmentedControl: UISegmentedControl {    var sortedViews: [UIView]!    var currentIndex: Int = 0    required init?(coder aDecoder: NSCoder) {        super.init(coder: aDecoder)        configure()    }    private func configure() {        sortedViews = self.subviews.sorted(by:{$0.frame.origin.x < $1.frame.origin.x})    }    func changeSelectedIndex(to newIndex: Int) {        currentIndex = newIndex        self.selectedSegmentIndex = UISegmentedControlNoSegment    }}

Making it Look Good

Now we have all of the tools in place to customize the UISegmentedControl.  Define a background color for the selected segment as selectedBackgroundColor.  In changeSelectedIndex, set the old selectedIndex’s background color to clear and set the new selected segment’s background color to the defined color.  Also add a call to changeSelectedIndex in configure to initialize our UISegmentedControl properly.

import UIKitclass CustomSegmentedControl: UISegmentedControl {        let selectedBackgroundColor = UIColor(red: 19/255, green: 59/255, blue: 85/255, alpha: 0.5)    var sortedViews: [UIView]!    var currentIndex: Int = 0        required init?(coder aDecoder: NSCoder) {        super.init(coder: aDecoder)        configure()    }       private func configure() {        sortedViews = self.subviews.sorted(by:{$0.frame.origin.x < $1.frame.origin.x})        changeSelectedIndex(to: currentIndex)    }        func changeSelectedIndex(to newIndex: Int) {        sortedViews[currentIndex].backgroundColor = UIColor.clear        currentIndex = newIndex        self.selectedSegmentIndex = UISegmentedControlNoSegment        sortedViews[currentIndex].backgroundColor = selectedBackgroundColor    }}

Finishing Touches

The last step is to add a few finishing touches to the configure function including the tint color, rounded corners, and font styling. This is the completed file:

import UIKitclass CustomSegmentedControl: UISegmentedControl {        let selectedBackgroundColor = UIColor(red: 19/255, green: 59/255, blue: 85/255, alpha: 0.5)    var sortedViews: [UIView]!    var currentIndex: Int = 0        required init?(coder aDecoder: NSCoder) {        super.init(coder: aDecoder)        configure()    }        private func configure() {        sortedViews = self.subviews.sorted(by:{$0.frame.origin.x < $1.frame.origin.x})        changeSelectedIndex(to: currentIndex)        self.tintColor = UIColor.white        self.layer.cornerRadius = 4        self.clipsToBounds = true        let unselectedAttributes = [NSForegroundColorAttributeName: UIColor.white,                                    NSFontAttributeName:  UIFont.systemFont(ofSize: 13, weight: UIFontWeightRegular)]        self.setTitleTextAttributes(unselectedAttributes, for: .normal)        self.setTitleTextAttributes(unselectedAttributes, for: .selected)    }        func changeSelectedIndex(to newIndex: Int) {        sortedViews[currentIndex].backgroundColor = UIColor.clear        currentIndex = newIndex        self.selectedSegmentIndex = UISegmentedControlNoSegment        sortedViews[currentIndex].backgroundColor = selectedBackgroundColor    }}

This process doesn’t involve a lot of code, but it certainly took me some time to find a solution that gave me what I wanted.  Hopefully this saves you some trouble and gives you a new way of thinking about UI customization using Swiftin iOS.

You can find the source code at https://github.com/JoeCoy/CustomSegmentedControl 

Build awesome things for fun.

Check out our current openings for your chance to make awesome things with creative, curious people.

Explore SEP Careers »

You Might Also Like