How To Make a Custom Control Tutorial: A Reusable Slider

Controls are the bread-and-butter of iOS apps. There are many provided in UIKit but this tutorial shows you how to make a custom control in Swift. By Mikael Konutgan.

Leave a rating/review
Save for later
Share
You are currently viewing page 4 of 5 of this article. Click here to view the first page.

Modifying Your Control With Core Graphics

First, you’ll update the graphics of the “track” that the slider thumbs move along.

Add another subclass of CALayer to the project just like before, this time calling it RangeSliderTrackLayer.

Open up the newly added file RangeSliderTrackLayer.swift, and replace its contents with the following:

import UIKit
import QuartzCore

class RangeSliderTrackLayer: CALayer {
    weak var rangeSlider: RangeSlider?
}

The code above adds a reference back to the range slider, just as you did previously for the thumb layer.

Open up RangeSlider.swift, locate the trackLayer property and modify it to be an instance of the new layer class, as below:

let trackLayer = RangeSliderTrackLayer()

Now find init and replace the method with the following:

init(frame: CGRect) {
    super.init(frame: frame)

    trackLayer.rangeSlider = self
    trackLayer.contentsScale = UIScreen.mainScreen().scale
    layer.addSublayer(trackLayer)

    lowerThumbLayer.rangeSlider = self
    lowerThumbLayer.contentsScale = UIScreen.mainScreen().scale
    layer.addSublayer(lowerThumbLayer)

    upperThumbLayer.rangeSlider = self
    upperThumbLayer.contentsScale = UIScreen.mainScreen().scale
    layer.addSublayer(upperThumbLayer)
}

The code above ensures that the new track layer has a reference to the range slider — and that the hideous background colors are no longer applied. :] Setting the contentsScale factor to match that of the device’s screen will ensure everything is crisp on retina displays.

There’s just one more bit — remove the red background of the control.

Open up ViewController.swift, locate the following line in viewDidLoad and remove it:

rangeSlider.backgroundColor = UIColor.redColor()

Build and run now…what do you see?

Screenshot #3

Do you see nothing? That’s good!

Good? What’s good about that? All of your hard work — gone?!?!

Don’t fret — you’ve just removed the gaudy test colors that were applied to the layers. Your controls are still there — but now you have a blank canvas to dress up your controls!

Since most developers like it when controls can be configured to emulate the look and feel of the particular app they are coding, you will add some properties to the slider to allow customization of the “look” of the control.

Open RangeSlider.swift and add the following properties just beneath the ones you added earlier:

var trackTintColor = UIColor(white: 0.9, alpha: 1.0)
var trackHighlightTintColor = UIColor(red: 0.0, green: 0.45, blue: 0.94, alpha: 1.0)
var thumbTintColor = UIColor.whiteColor()

var curvaceousness : CGFloat = 1.0

The purposes of the various color properties are fairly straightforward. And curvaceousness? Well, that one is in there for a bit of fun — you’ll find out what it does shortly! :]

Next, open up RangeSliderTrackLayer.swift.

This layer renders the track that the two thumbs slide on. It currently inherits from CALayer, which only renders a solid color.

In order to draw the track, you need to implement drawInContext: and use the Core Graphics APIs to perform the rendering.

Note: To learn about Core Graphics in depth, the Core Graphics 101 tutorial series from this site is highly recommended reading, as exploring Core Graphics is out of scope for this tutorial.

Add the following method to RangeSliderTrackLayer:

    
override func drawInContext(ctx: CGContext!) {
    if let slider = rangeSlider {
        // Clip
        let cornerRadius = bounds.height * slider.curvaceousness / 2.0
        let path = UIBezierPath(roundedRect: bounds, cornerRadius: cornerRadius)
        CGContextAddPath(ctx, path.CGPath)
        
        // Fill the track
        CGContextSetFillColorWithColor(ctx, slider.trackTintColor.CGColor)
        CGContextAddPath(ctx, path.CGPath)
        CGContextFillPath(ctx)
        
        // Fill the highlighted range
        CGContextSetFillColorWithColor(ctx, slider.trackHighlightTintColor.CGColor)
        let lowerValuePosition = CGFloat(slider.positionForValue(slider.lowerValue))
        let upperValuePosition = CGFloat(slider.positionForValue(slider.upperValue))
        let rect = CGRect(x: lowerValuePosition, y: 0.0, width: upperValuePosition - lowerValuePosition, height: bounds.height)
        CGContextFillRect(ctx, rect)
    }
}

Once the track shape is clipped, the background is filled in. After that, the highlighted range is filled in.

Build and run to see your new track layer rendered in all its glory! It should look like the following:

Screenshot #4

Play around with the various values for the exposed properties to see how they affect the rendering of the control.

If you’re still wondering what curvaceousness does, try changing that as well!

You’ll use a similar approach to draw the thumb layers.

Open up RangeSliderThumbLayer.swift and add the following method just below the property declarations:

override func drawInContext(ctx: CGContext!) {
    if let slider = rangeSlider {
        let thumbFrame = bounds.rectByInsetting(dx: 2.0, dy: 2.0)
        let cornerRadius = thumbFrame.height * slider.curvaceousness / 2.0
        let thumbPath = UIBezierPath(roundedRect: thumbFrame, cornerRadius: cornerRadius)

        // Fill - with a subtle shadow
        let shadowColor = UIColor.grayColor()
        CGContextSetShadowWithColor(ctx, CGSize(width: 0.0, height: 1.0), 1.0, shadowColor.CGColor)
        CGContextSetFillColorWithColor(ctx, slider.thumbTintColor.CGColor)
        CGContextAddPath(ctx, thumbPath.CGPath)
        CGContextFillPath(ctx)

        // Outline
        CGContextSetStrokeColorWithColor(ctx, shadowColor.CGColor)
        CGContextSetLineWidth(ctx, 0.5)
        CGContextAddPath(ctx, thumbPath.CGPath)
        CGContextStrokePath(ctx)

        if highlighted {
            CGContextSetFillColorWithColor(ctx, UIColor(white: 0.0, alpha: 0.1).CGColor)
            CGContextAddPath(ctx, thumbPath.CGPath)
            CGContextFillPath(ctx)
        }
    }
}

Once a path is defined for the shape of the thumb, the shape is filled in. Notice the subtle shadow which gives the impression the thumb hovers above the track. The border is rendered next. Finally, if the thumb is highlighted — that is, if it’s being moved — a subtle grey shading is applied.

One last thing before we build and run. Change the declaration of the highlighted property as follows:

var highlighted: Bool = false {
    didSet {
        setNeedsDisplay()
    }
}

Here, you define a property observer so that the layer is redrawn every time the highlighted property changes. That will change the fill color slightly for when the touch event is active.

Build and run once again; it’s looking pretty sharp and should resemble the screenshot below:

Screenshot #5

You can easily see that rendering your control using Core Graphics is really worth the extra effort. Using Core Graphics results in a much more versatile control compared to one that is rendered from images alone.

Handling Changes to Control Properties

So what’s left? The control now looks pretty snazzy, the visual styling is versatile, and it supports target-action notifications.

It sounds like you’re done — or are you?

Think for a moment about what happens if one of the range slider properties is set in code after it has been rendered. For example, you might want to change the slider range to some preset value, or change the track highlight to indicate a valid range.

Currently there is nothing observing the property setters. You’ll need to add that functionality to your control. You need to implement property observers that update the control’s frame or drawing. Open up RangeSlider.swift and change the property declarations of the following properties like this:

var minimumValue: Double = 0.0 {
    didSet {
        updateLayerFrames()
    }
}

var maximumValue: Double = 1.0 {
    didSet {
        updateLayerFrames()
    }
}

var lowerValue: Double = 0.2 {
    didSet {
        updateLayerFrames()
    }
}

var upperValue: Double = 0.8 {
    didSet {
        updateLayerFrames()
    }
}

var trackTintColor: UIColor = UIColor(white: 0.9, alpha: 1.0) {
    didSet {
        trackLayer.setNeedsDisplay()
    }
}

var trackHighlightTintColor: UIColor = UIColor(red: 0.0, green: 0.45, blue: 0.94, alpha: 1.0) {
    didSet {
        trackLayer.setNeedsDisplay()
    }
}

var thumbTintColor: UIColor = UIColor.whiteColor() {
    didSet {
        lowerThumbLayer.setNeedsDisplay()
        upperThumbLayer.setNeedsDisplay()
    }
}

var curvaceousness: CGFloat = 1.0 {
    didSet {
        trackLayer.setNeedsDisplay()
        lowerThumbLayer.setNeedsDisplay()
        upperThumbLayer.setNeedsDisplay()
    }
}

Basically, you need to call setNeedsDisplay for the affected layers depending on which property was changed. setLayerFrames is invoked for properties that affect the control’s layout.

Now, find updateLayerFrames and add the following to the top of the method:

CATransaction.begin()
CATransaction.setDisableActions(true)

Add the following to the very bottom of the method:

    
CATransaction.commit()

This code will wrap the entire frame update into one transaction to make the re-flow rendering smooth. It also disables implicit animations on the layer, just like we did before, so the layer frames are update immediately.

Since you are now updating the frames automatically, every time the upper and lower values change, find the following code in continueTrackingWithTouch and delete it:

// 3. Update the UI
CATransaction.begin()
CATransaction.setDisableActions(true)

updateLayerFrames()

CATransaction.commit()

That’s all you need to do in order to ensure the range slider reacts to property changes.

However, you now need a bit more code to test your new macros and make sure everything is hooked up and working as expected.

Open up ViewController.swift and add the following code to the end of viewDidLoad:

let time = dispatch_time(DISPATCH_TIME_NOW, Int64(NSEC_PER_SEC))
dispatch_after(time, dispatch_get_main_queue()) {
    self.rangeSlider.trackHighlightTintColor = UIColor.redColor()
    self.rangeSlider.curvaceousness = 0.0
}

This will update some of the control’s properties after a 1 second pause. You change the track highlight color to red, and change the shape of the range slider and its thumbs.

Build and run your project. After a second, you should see the range slider change from this:

to this:

How easy was that?

The code you just added to the view controller illustrates one of the most interesting, and often overlooked, points about developing custom controls – testing. When you are developing a custom control, it’s your responsibility to exercise all of its properties and visually verify the results. A good way to approach this is to create a visual test harness with various buttons and sliders, each of which connected to a different property of the control. That way you can modify the properties of your custom control in real time — and see the results in real time.

Mikael Konutgan

Contributors

Mikael Konutgan

Author

Over 300 content creators. Join our team.