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


19 comments:

  1. Hi,

    I would like when the number of segment is greater than 3, the user could scroll the segmentedControl.

    Is this posible with this framework? How could I do this?

    Thanks.

    ReplyDelete
    Replies
    1. Hi Buk,

      The framework itself doesn't support scrolling. But you can easily implement that feature by putting the SegmentView instance into a UIScrollView and adjust the scrollView's contentSize.

      Something like:
      scrollView.addSubView(segmentView)
      scrollView.contentSize = segmentView.frame.size

      Hope it helps.

      Cheers,
      Si

      Delete
    2. It works.

      Thank you very much.

      Delete
  2. Replies
    1. You can reach me via allenbryan11@gmail.com. ;)

      Delete
  3. hiiii


    when i add scrollview in this . its not working and show errors in the lines below

    scrollView.addSubView(segmentView)
    scrollView.contentSize = segmentView.frame.size

    plsss explain

    thanx

    ReplyDelete
    Replies
    1. Hi there,

      It's a bit hard to tell with just these two lines. Mind sending more code to my email address?

      Cheers,
      Si

      Delete
  4. Hi,

    Firstly thank you for this wonderful custom segmented control. However, I have two questions in mind.

    1) How do I change the font of the segmented Control?
    2) After performing the 'Expanding the framework', I've uncommented the

    segmentView.selectSegmentAtIndex(1)

    However, upon selecting index 1, the "selected bar" is still at 0. Is there a way to work around this?

    Thanks in advance!

    ReplyDelete
    Replies
    1. Hi there,

      For your 1), you can set font in init(frame: CGRect, separatorColour: UIColor, separatorWidth: CGFloat, segmentProperties: Dictionary?) method, where you need add your font into segmentProperties dictionary with key "keySegmentTitleFont"

      For your 2) Honestly I can't tell much without seeing your code, but maybe check if you have set a delegate for your segmentView?

      Delete
  5. organiseMode how to use?segmentView.organiseMode = .SegmentOrganiseVertical cant

    ReplyDelete
    Replies
    1. This is what I did in the sample, and it worked fine to me:
      self.segmentView.organiseMode = .SegmentOrganiseVertical

      I couldn't tell more without seeing more of your code, I'm afraid.

      Delete
  6. Hi,
    I want to customize segment size, here we have 3 segments with same size but i want to change the size. how can i do that?

    ReplyDelete
  7. Hi there,

    I'm afraid this framework doesn't support multi-sized segments.

    ReplyDelete
  8. Hi Si Ma,

    Your Segmented View looks cool! However, I am having an Objective C project, and after installing it through Cocoa Pods, I cannot find any header files. Is the framework compatible with Objective C as well?

    Erwin

    ReplyDelete
    Replies
    1. Hi Erwin,

      Sorry for the late reply. You can find the answer here https://developer.apple.com/library/ios/documentation/Swift/Conceptual/BuildingCocoaApps/MixandMatch.html

      Delete
  9. Is there a way to add a badge counter to each of these segment views? Also, need the flexibility to place the badge counter right after the text and not on the top right corner

    ReplyDelete
    Replies
    1. Not at the moment, I'm afraid. I'll see if I can add this feature into the next version.

      Delete
  10. "Change the SMSegmentView delegate method" - I think now it doesn't work. How can we do that in new version?

    Is it possible to put buttons under first segment and table under second?

    ReplyDelete