Tuesday 13 January 2015

Custom SegmentedControl In Swift

In this tutorial, we will use a third party framework named SMSegmentView to implement a unique and good looking segmented control for your Swift project. You will also learn how easy it is to expand this framework a little bit. ;)


A Short Introduction Of SMSegmentView

I know many of you may not be interested in the background, so please feel free to skip this part :)

Due to the standard look and the relatively low customisability of UISegmentedControl, the author of SMSegmentView wrote this framework for both developers and designer to make their apps more outstanding, which in another way, might also improve the user experience.

The framework supports both image and text in a single segment. Besides of the traditional horizontal look, it also allows you to arrange your segments in a vertical way. You can change the attributes of the text and segment as well. It is written in Swift and very easy to expand with new features, which is perfect to be used in your next project.

Now let start building something.

Build A Custom SegmentedView

First of all, you will need to find the framework here.

Then, create a new Xcode project with the Single View Application template. Name the project whatever you like, but in this tutorial, let's use SegmentedControlSample. Choose Swift as the language and iPhone as the devices.

Now drag the SMSegmentView folder inside the framework to your project and tick "Copy items if needed". And we are now ready to create our very customised segmented control. Pretty easy, huh? :D

Now let's create a property of the instance of SMSegmentView in ViewController.swift:

var segmentedControl: SMSegmentView!
ViewController.swift: Using the exclamation symbol is to tell the compiler: "Don't worry. I'll assign it a value later".

In viewDidLoad() we initialise the segmentedControl by using its frame:dividerColour:dividerWidth: segmentAppearance: method. Add the following code after super.viewDidLoad():


self.view.backgroundColor = UIColor(white: 0.95, alpha: 1.0)

// 1
let appearance = SMSegmentAppearance()
appearance.segmentOnSelectionColour = UIColor(red: 245.0/255.0, green: 174.0/255.0, blue: 63.0/255.0, alpha: 1.0)
appearance.segmentOffSelectionColour = UIColor.whiteColor()
appearance.titleOnSelectionFont = UIFont.systemFontOfSize(12.0)
appearance.titleOffSelectionFont = UIFont.systemFontOfSize(12.0)

appearance.contentVerticalMargin = 10.0

// 2
let segmentFrame = CGRect(x: self.margin, y: 120.0, width: self.view.frame.size.width - self.margin*2, height: 40.0)
self.segmentView = SMSegmentView(frame: segmentFrame, dividerColour: UIColor(white: 0.95, alpha: 0.3), dividerWidth: 1.0, segmentAppearance: appearance)

// 3
self.segmentedControl.addSegmentWithTitle("Segment 1", onSelectionImage: nil, offSelectionImage: nil)
self.segmentedControl.addSegmentWithTitle("Segment 2", onSelectionImage: nil, offSelectionImage: nil)
self.segmentedControl.addSegmentWithTitle("Segment 3", onSelectionImage: nil, offSelectionImage: nil)

// 4
self.segmentView.addTarget(self, action: #selector(ViewController.selectSegmentInSegmentView(_:)), forControlEvents: .ValueChanged)

self.view.addSubview(self.segmentedControl)
(1) Create an instance of SMSegmentAppearance, where you can specify some UI attributes.

(2) Call the initialiser of SMSegmentView. The parameter segmentAppearance reads the SMSegmentAppearance instance we created in step 1.

(3)We add three segments to our segmentedControler. In this demo, we do not need to assign images to our segment. But in real practice, you can add them if you want.

(4)We set an action to react to UIControlEvents.ValueChanged. After this step, don't forget to add the selector selectSegmentInSegmentView(_:) in the ViewController class:
func selectSegmentInSegmentView(segmentView: SMSegmentView) {
    print("Select segment at index: \(segmentView.selectedSegmentIndex)")

}


Now you can give it a try. If everything goes well, you will see the simulator show this when you select a segment:


Hmm... It looks not too bad, but not as good as it is expected, either :(

Don't worry. We haven't finished, yet.

Add the following code right above you added new segment to the segmentedControl:
self.segmentedControl.layer.borderColor = UIColor(white: 0.9, alpha: 1.0).CGColor
self.segmentedControl.layer.borderWidth = 1.0
As SMSegmentView is a subclass of UIView, we can use UIView's methods to give it a border.


Expanding The Framework A Little...

Now let's expand it a little bit to give it some animation when we select at a segment. First of all, let add another property to our ViewController:
var seletionBar: UIView = UIView()
And add the following code at the bottom of viewDidLoad().
self.seletionBar.frame = CGRect(x: 0.0, y: 0.0, width: self.segmentedControl.frame.size.width/CGFloat(self.segmentedControl.numberOfSegments), height: 5.0)
self.seletionBar.backgroundColor = UIColor(white: 0.5, alpha: 0.6)
You'll know how we are gonna use this view right as follows. Change the SMSegmentView delegate method to:
func segmentView(segmentView: SegmentView didSelectSegmentAtIndex index: Int) {
    
    // 1
    let placeSelectionBar = { () -> () in
        var barFrame = self.seletionBar.frame
        barFrame.origin.x = barFrame.size.width * CGFloat(segmentIndex)
        self.seletionBar.frame = barFrame
    }

    // 2
    if self.seletionBar.superview == nil {
        self.segmentedControl.addSubview(self.seletionBar)
        placeSelectionBar()
    }
    else {
        UIView.animateWithDuration(0.3, animations: {
        placeSelectionBar()
        })
    }
}
(1) We are using a closure to calculate where our bar should be placed.
(2) If no segment has been selected, yet, we will need to add the selectionBar view to our segmentedControl before put the bar on the right place. Otherwise, we animatedly reposition the bar view.

Let's run the app and see how it goes now.



It looks more dynamic now. Pretty easy stuff to make the change, right? ;)


A Little Bit More Words At The End

If you feel sick of the traditional horizontal arrangement of the segments, this framework also provides you a vertical way. All you need to do is to assign its organiseMode with .SegmentOrganiseVertical value after the view gets initialised.

Find the sample comes with the framework and you will know more about it.

Happy Coding :D