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 2 of 5 of this article. Click here to view the first page.

Adding Default Control Properties

Open up RangeSlider.swift and replace the code with the following:

import UIKit

class RangeSlider: UIControl {
    var minimumValue = 0.0
    var maximumValue = 1.0
    var lowerValue = 0.2
    var upperValue = 0.8
}

These four properties are all you need to describe the state of this control, providing maximum and minimum values for the range, along with the upper and lower values set by the user.

Well-designed controls should define some default property values, or else your control will look a little strange when it draws on the screen! You did that above as well.

Now it’s time to work on the interactive elements of your control; namely, the thumbs to represent the high and low values, and the track the thumbs slide on.

Images vs. CoreGraphics

There are two main ways that you can render controls on-screen:

  1. Images – create images that represent the various elements of your control
  2. CoreGraphics – render your control using a combination of layers and CoreGraphics

There are pros and cons to each technique, as outlined below:

Images — constructing your control using images is probably the simplest option in terms of authoring the control — as long as you know how to draw! :] If you want your fellow developers to be able to change the look and feel of your control, you would typically expose these images as UIImage properties.

Using images provides the most flexibility to developers who will use your control. Developers can change every single pixel and every detail of your control’s appearance, but this requires good graphic design skills — and it’s difficult to modify the control from code.

Core Graphics — constructing your control using Core Graphics means that you have to write the rendering code yourself, which will require a bit more effort. However, this technique allows you to create a more flexible API.

Using Core Graphics, you can parameterize every feature of your control, such as colors, border thickness, and curvature — pretty much every visual element that goes into drawing your control! This approach allows developers who use your control to fully tailor it to suit their needs.

In this tutorial you’ll use the second technique — rendering the control using Core Graphics.

Note: Interestingly, Apple tend to opt for using images in their controls. This is most likely because they know the size of each control and don’t tend to want to allow too much customization. After all, they want all apps to end up with a similar look-and-feel.

Open up RangeSlider.swift and add the following import to the top of the file, just below import UIKit:

import QuartzCore

Add the following properties to RangeSlider, just after the ones we defined above:

let trackLayer = CALayer()
let lowerThumbLayer = CALayer()
let upperThumbLayer = CALayer()

var thumbWidth: CGFloat {
    return CGFloat(bounds.height)
}

These three layers — trackLayer, lowerThumbLayer, and upperThumbLayer — will be used to render the various components of your slider control. thumbWidth will be used for layout purposes.

Next up are some default graphical properties of the control itself.

Inside the RangeSlider class, add an initializer and the following helper functions:

override init(frame: CGRect) {
    super.init(frame: frame)
    
    trackLayer.backgroundColor = UIColor.blueColor().CGColor
    layer.addSublayer(trackLayer)
    
    lowerThumbLayer.backgroundColor = UIColor.greenColor().CGColor
    layer.addSublayer(lowerThumbLayer)
    
    upperThumbLayer.backgroundColor = UIColor.greenColor().CGColor
    layer.addSublayer(upperThumbLayer)
    
    updateLayerFrames()
}

required init(coder: NSCoder) {
    super.init(coder: coder)
}

func updateLayerFrames() {
    trackLayer.frame = bounds.rectByInsetting(dx: 0.0, dy: bounds.height / 3)
    trackLayer.setNeedsDisplay()
    
    let lowerThumbCenter = CGFloat(positionForValue(lowerValue))
    
    lowerThumbLayer.frame = CGRect(x: lowerThumbCenter - thumbWidth / 2.0, y: 0.0,
      width: thumbWidth, height: thumbWidth)
    lowerThumbLayer.setNeedsDisplay()
    
    let upperThumbCenter = CGFloat(positionForValue(upperValue))
    upperThumbLayer.frame = CGRect(x: upperThumbCenter - thumbWidth / 2.0, y: 0.0,
        width: thumbWidth, height: thumbWidth)
    upperThumbLayer.setNeedsDisplay()
}

func positionForValue(value: Double) -> Double {
    return Double(bounds.width - thumbWidth) * (value - minimumValue) /
        (maximumValue - minimumValue) + Double(thumbWidth / 2.0)
}

The initializer simply creates three layers and adds them as children of the control’s root layer, then updateLayerFrames, well, updates the layer frames to fit! :]

Finally, positionForValue maps a value to a location on screen using a simple ratio to scale the position between the minimum and maximum range of the control.

Next, override frame, and implement a property observer by adding the following to RangeSlider.swift:

override var frame: CGRect {
    didSet {
        updateLayerFrames()
    }
}

The property observer updates the layer frames when the frame changes. This is necessary when the control is initialized with a frame that is not its final frame like in ViewController.swift.

Build and run your app; your slider is starting to take shape! It should look similar to the screenshot below:

Remember, red is the background of the entire control. Blue is the “track” for the slider and green will be the two thumb points for the upper and lower values.

Your control is starting to take shape visually, but almost every control provides a way for the app user to interact with it.

For your control, the user must be able to drag each thumb to set the desired range of the control. You’ll handle those interactions, and update both the UI and the properties exposed by the control.

Adding Interactive Logic

The interaction logic needs to store which thumb is being dragged, and reflect that in the UI. The control’s layers are a great place to put this logic.

As before, create a new Cocoa Touch Class in Xcode named RangeSliderThumbLayer which is a subclass of CALayer.

Replace the contents of the newly added RangeSliderThumbLayer.swift with the following:

import UIKit
import QuartzCore

class RangeSliderThumbLayer: CALayer {
    var highlighted = false
    weak var rangeSlider: RangeSlider?
}

This simply adds two properties: one that indicates whether this thumb is highlighted, and one that is a reference back to the parent range slider. Since the RangeSlider owns the two thumb layers, the back reference is a weak variable to avoid a retain cycle.

Open RangeSlider.swift and change the type of the lowerThumbLayer and upperThumbLayer properties, by replacing their definitions with the following:

let lowerThumbLayer = RangeSliderThumbLayer()
let upperThumbLayer = RangeSliderThumbLayer()

Still in RangeSlider.swift, find init and add the following lines to it:

lowerThumbLayer.rangeSlider = self
upperThumbLayer.rangeSlider = self

The above code simply sets the layer’s rangeSlider property to populate the back reference to self.

Build and run your project; check to see if everything still looks the same.

Now that you have the slider’s thumbs layers in place using RangeSliderThumbLayer, you need to add the ability for the user to drag the slider thumbs around.

Mikael Konutgan

Contributors

Mikael Konutgan

Author

Over 300 content creators. Join our team.