Custom Control Tutorial for iOS and Swift: A Reusable Knob

Custom Control Tutorial for iOS and Swift: A Reusable Knob

Update note: This tutorial was updated for iOS 8 and Swift by Mikael Konutgan. Original post by Tutorial Team member Sam Davies.

Custom UI controls are extremely useful when you need some new functionality in your app — especially when they’re generic enough to be reusable in other apps.

We have an excellent tutorial providing an introduction to custom UI Controls in Swift. That tutorial walks you through the creation of a custom double-ended UISlider that lets you select a range with start and end values.

This custom control tutorial takes that concept a bit further and covers the creation of a control kind of like a circular slider inspired by a control knob, such as those found on a mixer:


UIKit provides the UISlider control, which lets you set a floating point value within a specified range. If you’ve used any iOS device, then you’ve probably used a UISlider to set volume, brightness, or any one of a multitude of other variables. The project you’ll build today will have exactly the same functionality, but in a circular form.

Prepare yourself for bending straight lines into bezier curves, and let’s get started!

Getting Started

First, download the starter project here. This is a simple single view application. The storyboard has a few controls that are wired up to the main view controller. You’ll use these controls later in the tutorial to demonstrate the different features of the knob control.

Build and run your project just to get a sense of how everything looks before you dive into the coding portion; it should look like the screenshot below:


To create the class for the knob control, click File/New/File… and select iOS/Source/Cocoa Touch Class. On the next screen, specify the class name as Knob, have the class inherit from UIControl and make sure the language is Swift. Click Next, choose the “KnobDemo” directory and click Create.

Before you can write any code for the new control, you must first add it to the view controller so you can see how it evolves visually.

Open up ViewController.swift and add the following property just after the other ones:

var knob: Knob!

Now replace viewDidLoad with the following code:

override func viewDidLoad() {
  knob = Knob(frame: knobPlaceholder.bounds)

This creates the knob and adds it to the placeholder in the storyboard. The knobPlaceholder property is already wired up as an IBOutlet.

Open Knob.swift and replace the boiler-plate class definition with the following code:

public class Knob: UIControl {
  public override init(frame: CGRect) {
    super.init(frame: frame)
    backgroundColor = tintColor
  public required init(coder aDecoder: NSCoder) {
    fatalError("init(coder:) has not been implemented")

This code makes the class public, defines the two initializers and sets the background color of the knob so that you can see it on the screen. It is required for a view that defines init(frame:) to also define init(coder:), but you will not deal with that method now, so you just raise an error. This is the exact code Xcode suggests if you don’t define init(coder:).

Build and run your app and you’ll see the following:


Okay, you have the basic building blocks in place for your app. Time to work on the API for your control!

Designing Your Control’s API

Your main reason for creating a custom UI control is to create a handy and reusable component. It’s worth taking a bit of time up-front to plan a good API for your control; developers using your component should understand how to use it from looking at the API alone, without any need to open up the source code. This means that you’ll need to document your API as well!

The public functions and properties of your custom control is its API.

In Knob.swift, add the following code to the Knob class:

private var backingValue: Float = 0.0
/** Contains the receiver’s current value. */
public var value: Float {
  get { return backingValue }
  set { setValue(newValue, animated: false) }
/** Sets the receiver’s current value, allowing you to animate the change visually. */
public func setValue(value: Float, animated: Bool) {
  if value != backingValue {
    backingValue = min(maximumValue, max(minimumValue, value))
/** Contains the minimum value of the receiver. */
public var minimumValue: Float = 0.0
/** Contains the maximum value of the receiver. */
public var maximumValue: Float = 1.0
/** Contains a Boolean value indicating whether changes
    in the sliders value generate continuous update events. */
public var continuous = true
  • value, minimumValue and maximumValue simply set the basic operating parameters of your control.
  • setValue(_:animated:) lets you set the value of the control programmatically, while the additional boolean parameter indicates whether or not the change in value is to be animated. You want to use mostly the same code when you set the value or call setValue(_:animated:). To achieve this, you use a private backingValue property that holds the actual value. value then just returns that backing value when you get it and calls setValue(_:animated:) with false when you set it. Finally you ensure that the value is bounded within the limits associated with the control before actually setting it.
  • If continuous is set to true, then the control calls back repeatedly as the value changes; if it is set to false, the the control only calls back once after the user has finished interacting with the control.

You’ll ensure that these properties behave appropriately as you fill out the knob control implementation later on in this tutorial.

Those comments might seem superfluous, but Xcode can pick them up and show them in a tooltip, like so:


Code-completion tips like this are a huge time-saver for the developers who use your control, whether that’s you, your teammates or other people!

Now that you’ve defined the API of your control, it’s time to get cracking on the visual design.

Setting the Appearance of Your Control

Our previous tutorial compares Core Graphics and images as two potential methods to set the appearance of your custom control. However, that’s not an exhaustive list; in this custom UI tutorial, you’ll explore a third option to control the visuals of your control: Core Animation layers.

Whenever you use a UIView, it’s backed by a CALayer, which helps iOS optimize the rendering on the GPU. CALayer objects manage visual content and are designed to be incredibly efficient for all types of animations.

Your knob control will be made up of two CALayer objects: one for the track, and one for the pointer itself. This will result in extremely good performance for your animation, as you’ll see later.

The diagram below illustrates the basic construction of your knob control:


The blue and red squares represent the two CALayer objects; the blue layer contains the track of the knob control, and the red layer the pointer. When overlaid, the two layers create the desired appearance of a moving knob. The difference in coloring above is only to illustrate the different layers of the control — not to worry, your control will look much nicer than that. ;]

The reason to use two separate layers becomes obvious when the pointer moves to represent a new value. All you need to do is rotate the layer containing the pointer, which is represented by the red layer in the diagram above.

It’s cheap and easy to rotate layers in Core Animation. If you chose to implement this using Core Graphics and override drawRect, the entire knob control would be re-rendered in every step of the animation. This is a very expensive operation, and will likely result in sluggish animation, particularly if changes to the control’s value invoke other re-calculations within your app.

To keep the appearance parts separate from the control parts, add a new private class to the end of Knob.swift:

private class KnobRenderer {

This class will keep track of the code associated with rendering the knob itself. That will add a clean separation between the public Knob class and its internal workings.

Next, add the following code inside the KnobRenderer definition:

var strokeColor: UIColor {
  get {
    return UIColor(CGColor: trackLayer.strokeColor)
  set(strokeColor) {
    trackLayer.strokeColor = strokeColor.CGColor
    pointerLayer.strokeColor = strokeColor.CGColor
var lineWidth: CGFloat = 1.0
let trackLayer = CAShapeLayer()
var startAngle: CGFloat = 0.0
var endAngle: CGFloat = 0.0
let pointerLayer = CAShapeLayer()
var backingPointerAngle: CGFloat = 0.0
var pointerAngle: CGFloat {
  get { return backingPointerAngle }
  set { setPointerAngle(newValue, animated: false) }
func setPointerAngle(pointerAngle: CGFloat, animated: Bool) {
  self.backingPointerAngle = pointerAngle
var pointerLength: CGFloat = 0.0

Most of these properties deal with the visual appearance of the knob, with two CAShapeLayer properties representing the two layers which make up the overall appearance of the control. The strokeColor property just delegates to the strokeColor of the two layers and you’re using the same pattern as above with the backingPointerAngle, pointerAngle properties along with the setPointerAngle(_:animated:) method.

Also add an initializer to the class:

init() {
  trackLayer.fillColor = UIColor.clearColor().CGColor
  pointerLayer.fillColor = UIColor.clearColor().CGColor

This sets the appearance of the two layers as transparent.

You’ll create the two shapes that make up the overall knob as CAShapeLayer objects. These are a special subclasses of CALayer that draw a bezier path using anti-aliasing and some optimized rasterization. This makes CAShapeLayer an extremely efficient way to draw arbitrary shapes.

Add the following two methods to the KnobRenderer class:

func updateTrackLayerPath() {
  let arcCenter = CGPoint(x: trackLayer.bounds.width / 2.0, y: trackLayer.bounds.height / 2.0)
  let offset = max(pointerLength, trackLayer.lineWidth / 2.0)
  let radius = min(trackLayer.bounds.height, trackLayer.bounds.width) / 2.0 - offset;
  trackLayer.path = UIBezierPath(arcCenter: arcCenter, radius: radius, startAngle: startAngle, endAngle: endAngle, clockwise: true).CGPath
func updatePointerLayerPath() {
  let path = UIBezierPath()
  path.moveToPoint(CGPoint(x: pointerLayer.bounds.width - pointerLength - pointerLayer.lineWidth / 2.0, y: pointerLayer.bounds.height / 2.0))
  path.addLineToPoint(CGPoint(x: pointerLayer.bounds.width, y: pointerLayer.bounds.height / 2.0))
  pointerLayer.path = path.CGPath

updateTrackLayerPath creates an arc between the startAngle and endAngle values with a radius that ensures the pointer will fit within the layer, and positions it on the center of the trackLayer. Once you create the UIBezierPath, you then use the CGPath property to set the path on the appropriate CAShapeLayer.

CGPathRef is the Core Graphics equivalent of UIBezierPath. Since UIBezierPath has a nicer, more modern API, you use that to initially create the path, and then convert it to a CGPathRef.

updatePointerLayerPath creates the path for the pointer at the position where angle is equal to zero. Again, you create a UIBezierPath, convert it to a CGPathRef and assign it to the path property of your CAShapeLayer. Since the pointer is a simple straight line, all you need to draw the pointer are moveToPoint and addLineToPoint.

Calling these methods redraws the two layers; this must happen when any of the properties used by these methods are modified. To do that, you’ll need to implement property observers for the properties you added to the API for the renderer to use.

First create a single update method:

func update() {
  trackLayer.lineWidth = lineWidth
  pointerLayer.lineWidth = lineWidth

You’ll call this method to update both layers and their line widths.

Now change the lineWidth, startAngle, endAngle, and pointerLength properties of KnobRenderer as follows:

var lineWidth: CGFloat = 1.0 {
  didSet { update() }
var startAngle: CGFloat = 0.0 {
  didSet { update() }
var endAngle: CGFloat = 0.0 {
  didSet { update() }
var pointerLength: CGFloat = 0.0 {
didSet { update() }

You may have noticed that the two methods for updating the shape layer paths rely on one more property which has never been set — namely, the bounds of each of the shape layers. Since you never set the CAShapeLayer bounds, they currently have a zero-sized bounds.

Add a new method to KnobRenderer:

func update(bounds: CGRect) {
  let position = CGPoint(x: bounds.width / 2.0, y: bounds.height / 2.0)
  trackLayer.bounds = bounds
  trackLayer.position = position
  pointerLayer.bounds = bounds
  pointerLayer.position = position

The above method takes a bounds rectangle, resizes the layers to match and positions the layers in the center of the bounding rectangle. As you’ve changed a property that affects the paths, you must call the update() method manually.

Although the renderer isn’t quite complete, there’s enough here to demonstrate the progress of your control. Add a property to hold an instance of your renderer to the Knob class:

private let knobRenderer = KnobRenderer()

Then add the following method, also to Knob:

func createSublayers() {
  knobRenderer.strokeColor = tintColor
  knobRenderer.startAngle = -CGFloat(M_PI * 11.0 / 8.0);
  knobRenderer.endAngle = CGFloat(M_PI * 3.0 / 8.0);
  knobRenderer.pointerAngle = knobRenderer.startAngle;
  knobRenderer.lineWidth = 2.0
  knobRenderer.pointerLength = 6.0

The above method sets the knob renderer’s size, then adds the two layers as sublayers of the control’s layer. You’ve temporarily hard-coded the startAngle and endAngle properties just so that your view will render for testing.

An empty view would be taking the iOS 7/8 design philosophy a step too far, so you need to make sure createSublayers is called when the knob control is being constructed. Add the following line to the initializer:


You can also remove the following line from the initializer since it is no longer required:

backgroundColor = tintColor

Your initializer should now look like this:

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

Build and run your app, and your control should look like the one below:


It’s not yet complete, but you can see the basic framework of your control taking shape.

Exposing Appearance Properties in the API

Currently, the developer using your control has no way of changing the control’s appearance, since all of the properties which govern the look of the control are hidden away in the private renderer.

To fix this, add the following properties to the Knob class:

/** Specifies the angle of the start of the knob control track. Defaults to -11π/8 */
public var startAngle: CGFloat {
  get { return knobRenderer.startAngle }
  set { knobRenderer.startAngle = newValue }
/** Specifies the end angle of the knob control track. Defaults to 3π/8 */
public var endAngle: CGFloat {
  get { return knobRenderer.endAngle }
  set { knobRenderer.endAngle = newValue }
/** Specifies the width in points of the knob control track. Defaults to 2.0 */
public var lineWidth: CGFloat {
  get { return knobRenderer.lineWidth }
  set { knobRenderer.lineWidth = newValue }
/** Specifies the length in points of the pointer on the knob. Defaults to 6.0 */
public var pointerLength: CGFloat {
  get { return knobRenderer.pointerLength }
  set { knobRenderer.pointerLength = newValue }

Just as before, there are plenty of comments to assist developers when they use the control.

The four properties are fairly straightforward and simply proxy for the properties in the renderer. Since the control itself doesn’t actually need backing variables for these properties, it can rely on the renderer to store the values instead.

To test that the new API bits are working as expected, add the following code to the end of viewDidLoad in ViewController.swift:

knob.lineWidth = 4.0
knob.pointerLength = 12.0

Build and run your project again; you’ll see that the line thickness and the length of the pointer have both increased, as shown below:


Changing Your Control’s Color

You may have noticed that you didn’t create any color properties on the public API of the control — and for good reason. There is a property on UIView, tintColor that you will use. In fact, you’re already using it to set the color of the knob in the first place — check the knobRenderer.strokeColor = tintColor line in createSublayers if you don’t believe me. :]

So you might expect that adding the following line to the end of viewDidLoad inside ViewController.swift will change the color of the control:

view.tintColor = UIColor.redColor()

Add the code above and build and run your project, you’ll quickly be disappointed. However, the UIButton has updated appropriately, as demonstrated below:


Although you’re setting the renderer’s color when the UI is created, it won’t be updated when the tintColor changes. Luckily, this is really easy to fix.

Add the following function to the Knob class:

public override func tintColorDidChange() {
  knobRenderer.strokeColor = tintColor

Whenever you change the tintColor property of a view, UIKit calls tintColorDidChange on all views beneath the current view in the view hierarchy that haven’t had their tintColor property set manually. So to listen for tintColor updates anywhere above the view in the current hierarchy, all you have to do is implement tintColorDidChange in your code and update the view’s appearance appropriately.

Build and run your project; you’ll see that the red tint has been picked up by your control as shown below:


Setting the Control’s Value Programmatically

Although your knob looks pretty nice, it doesn’t actually do anything. In this next phase you’ll modify the control to respond to programmatic interactions — that is, when the value property of the control changes.

At the moment, the value of the control is saved when the value property is modified directly or when you call setValue(_:animated:). However, there isn’t any communication with the renderer, and the control won’t re-render.

The renderer has no concept of value; it deals entirely in angles. You’ll need to update setValue(_:animated:) in Knob so that it converts the value to an angle and passes it to the renderer.

Go the the Knob class and update setValue(_:animated:) so that it matches the following code:

public func setValue(value: Float, animated: Bool) {
  if value != self.value {
    // Save the value to the backing value
    // Make sure we limit it to the requested bounds
    self.backingValue = min(self.maximumValue, max(self.minimumValue, value))
    // Now let's update the knob with the correct angle
    let angleRange = endAngle - startAngle
    let valueRange = CGFloat(maximumValue - minimumValue)
    let angle = CGFloat(value - minimumValue) / valueRange * angleRange + startAngle
    knobRenderer.setPointerAngle(angle, animated: animated)

The code above now works out the appropriate angle for the given value by mapping the minimum and maximum value range to the minimum and maximum angle range and sets the pointerAngle property on the renderer.

Note you’re just passing the value of animated to the renderer, but nothing is actually animating at the moment — you’ll fix this in the next section.

Although the pointerAngle property is being updated, it doesn’t yet have any effect on your control. When the pointer angle is set, the layer containing the pointer should rotate to the specified angle to give the impression that the pointer has moved.

Update setPointerAngle(_:animated:) to the following:

func setPointerAngle(pointerAngle: CGFloat, animated: Bool) {
  pointerLayer.transform = CATransform3DMakeRotation(pointerAngle, 0.0, 0.0, 0.1)
  self.backingPointerAngle = pointerAngle

This simply creates a rotation transform which rotates the layer around the z-axis by the specified angle.

The transform property of CALayer expects to be passed a CATransform3D, not a CGAffineTransform like UIView. This means that you can perform transformations in three dimensions.

CGAffineTransform uses a 3×3 matrix and CATransform3D uses a 4×4 matrix; the addition of the z-axis requires the extra values. At their core, 3D transformations are simply matrix multiplications. You can read more about matrix multiplication in this Wikipedia article.

To demonstrate that your transforms work, you’re going to link the UISlider present in the starter project with the knob control in the view controller. As you adjust the slider, the value of the knob will change appropriately.

The UISlider has already been linked to sliderValueChanged so update that method to the following implementation:

@IBAction func sliderValueChanged(slider: UISlider) {
  knob.value = slider.value

The method will simply update the knob value with the new slider value.

Build and run your project; change the value of the UISlider and you’ll see the pointer on the knob control move to match as shown below:


There’s a little bonus here — your control is animating, despite the fact that you haven’t started coding any of the animations yet! What gives?

Core Animation is quietly calling implicit animations on your behalf. When you change certain properties of CALayer — including transform — the layer animates smoothly from the current value to the new value. Remember the CA in CALayer stands for Core Animation!

Usually this functionality is really cool; you get nice looking animations without doing any work. However, you want a little more control, so you’ll animate things yourself.

Update setPointerAngle as follows:

func setPointerAngle(pointerAngle: CGFloat, animated: Bool) {
  pointerLayer.transform = CATransform3DMakeRotation(pointerAngle, 0.0, 0.0, 0.1)
  self.backingPointerAngle = pointerAngle

To prevent these implicit animations, you wrap the property change in a CATransaction and disable animations for that interaction.

Build and run your app once more; you’ll see that as you move the UISlider, the knob follows instantaneously.

Animating Changes to the Control’s Value

At the moment, setting the animated parameter to true has no effect on your control. To enable this bit of functionality, update setPointerAngle once again as follows:

func setPointerAngle(pointerAngle: CGFloat, animated: Bool) {
  pointerLayer.transform = CATransform3DMakeRotation(pointerAngle, 0.0, 0.0, 0.1)
  if animated {
    let midAngle = (max(pointerAngle, self.pointerAngle) - min(pointerAngle, self.pointerAngle) ) / 2.0 + min(pointerAngle, self.pointerAngle)
    let animation = CAKeyframeAnimation(keyPath: "transform.rotation.z")
    animation.duration = 0.25
    animation.values = [self.pointerAngle, midAngle, pointerAngle]
    animation.keyTimes = [0.0, 0.5, 1.0]
    animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
    pointerLayer.addAnimation(animation, forKey: nil)
  self.backingPointerAngle = pointerAngle

The difference here is when animated is set; if you had left this section with its implicit animation, the direction of rotation would be chosen to minimize the distance travelled. This means that animating between 0.98 and 0.1 wouldn’t rotate your layer counter-clockwise, but instead rotate clockwise over the end of the track, and into the bottom, which is not what you want!

In order to specify the rotation direction, you need to use a keyframe animation. That’s simply an animation where you specify some in-between points in addition to the usual start and end points.

Core Animation supports keyframe animations; in the above method, you’ve created a new CAKeyFrameAnimation and specified that the property to animate is the rotation around the z-axis with transform.rotation.z as its keypath.

Next, you specify three angles through which the layer should rotate: the start point, the mid-point and finally the end point. Along with that, there’s an array specifying the normalized times (as percentages) at which to reach those values. Adding the animation to the layer ensures that once the transaction is committed the animation will start.

In order to see this new functionality in action, you can use the “Random Value” button which is part of the app’s main view controller. This button causes the slider and knob controls to move to a random value, and uses the current setting of the animate switch to determine whether or not the change to the new value should be instantaneous or animated.

Update randomButtonTouched in ViewController to match the following:

@IBAction func randomButtonTouched(button: UIButton) {
  let randomValue = Float(arc4random_uniform(101)) / 100.0
  knob.setValue(randomValue, animated: animateSwitch.on)
  valueSlider.setValue(randomValue, animated: animateSwitch.on)

The above method generates a random value between 0.00 and 1.00 and sets the value on both controls. It then inspects the on property of animateSwitch to determine whether or not to animate the transition to the new value.

Build and run your app; tap the Random Value button a few times with the animate switch toggled on, then tap the Random Value button a few times with the animate switch toggled off to see the difference the animated parameter makes.

Updating the Label

Open ViewController.swift and add a method to update the label:

func updateLabel() {
  valueLabel.text = NSNumberFormatter.localizedStringFromNumber(knob.value, numberStyle: .DecimalStyle)

This will show the current value selected by the knob control. Next, add a call to this new method at the end of both sliderValueChanged and randomButtonTouched like this:


Finally, update the initial value of the knob and the label to be the initial value of the slider so that all they are in sync when the app starts. Add the following code to the end of viewDidLoad:

knob.value = valueSlider.value

Build and run, and run a few tests to make sure the label shows the correct value.


Responding to Touch Interaction

The knob control you’ve built responds extremely well to programmatic interaction, but that alone isn’t terribly useful for a UI control. In this final section you’ll see how to add touch interaction using a custom gesture recognizer.

As you touch the screen, iOS delivers a series of UITouch events to the appropriate objects. When a touch occurs inside of a view with one or more gesture recognizers attached, the touch event is delivered to the gesture recognizers for interpretation. Gesture recognizers determine whether a given sequence of touch events matches a specific pattern; if so, they send an action message to a specified target.

Apple provides a set of pre-defined gesture recognizers, such as tap, pan and pinch. However, there’s nothing to handle the single-finger rotation you need for the knob control. Looks like it’s up to you to create your own custom gesture recognizer.

Add a new private class to the end of Knob.swift:

import UIKit.UIGestureRecognizerSubclass
private class RotationGestureRecognizer: UIPanGestureRecognizer {

This custom gesture recognizer will behave like a pan gesture recognizer; it will track a single finger dragging across the screen and update the location as required. For this reason, it subclasses UIPanGestureRecognizer.

You’ll also need the import statement so you can override some gesture recognizer methods later.

Note: You might be wondering why you’re adding all these private classes to Knob.swift rather than the usual one-class-per-file. For this project, it makes it easy to distribute just a single file to anyone who wants to use the control. You don’t have to worry about forgetting to include all your supplementary helper classes or leaving something behind.

Add the following new property to your new RotationGestureRecognizer class:

var rotation: CGFloat = 0.0

rotation represents the touch angle of the line which joins the current touch point to the center of the view to which the gesture recognizer is attached, as demonstrated in the following diagram:


There are three methods of interest when subclassing UIGestureRecognizer: they represent the time that the touches begin, the time they move and the time they end. You’re only interested when the gesture starts and when the user’s finger moves on the screen.

Add the following two methods to RotationGestureRecognizer:

override func touchesBegan(touches: NSSet!, withEvent event: UIEvent!) {
  super.touchesBegan(touches, withEvent: event)
override func touchesMoved(touches: NSSet!, withEvent event: UIEvent!) {
  super.touchesMoved(touches, withEvent: event)

Both of these methods call through to their super equivalent, and then call a utility function which you’ll add next:

func updateRotationWithTouches(touches: NSSet!) {
  let touch = touches.anyObject() as UITouch
  let location = touch.locationInView(self.view)
  self.rotation = rotationForLocation(location)
func rotationForLocation(location: CGPoint) -> CGFloat {
  let offset = CGPoint(x: location.x - view!.bounds.midX, y: location.y - view!.bounds.midY)
  return atan2(offset.y, offset.x)

updateRotationWithTouches takes the NSSet of touches and extracts one using anyObject. It then uses locationInView to translate the touch point into the coordinate system of the view associated with this gesture recognizer. It then updates the rotation property using rotationForLocation, which uses some simple geometry to find find the angle as demonstrated below:


x and y represent the horizontal and vertical positions of the touch point within the control. The tangent of the rotation, that is the touch angle is equal to h / w, so to calculate rotation all you need to do is establish the following lengths:

  • h = y - (view height) / 2.0 (since the angle should increase in a clockwise direction)
  • w = x - (view width) / 2.0

rotationForLocation: performs this calculation for you, and returns the angle required.

Finally, your gesture recognizer should only work with one touch at a time. Add the following initializer override to the class:

override init(target: AnyObject, action: Selector) {
  super.init(target: target, action: action)
  minimumNumberOfTouches = 1
  maximumNumberOfTouches = 1

This initializer limits the number of recognized touches to just one.

Wiring Up the Custom Gesture Recognizer

Now that you’ve completed the custom gesture recognizer, you just need to wire it up to the knob control.

Update init(frame:) of Knob to the following:

public override init(frame: CGRect) {
  super.init(frame: frame)
  let gr = RotationGestureRecognizer(target: self, action: "handleRotation:")

The two lines added create a gesture recognizer in the familiar way: simply create a recognizer, specify where it should call back when activated then add it to the view.

Next, add the following method to Knob:

func handleRotation(sender: AnyObject) {
  let gr = sender as RotationGestureRecognizer
  // 1. Mid-point angle
  let midPointAngle = (2.0 * CGFloat(M_PI) + self.startAngle - self.endAngle) / 2.0 + self.endAngle
  // 2. Ensure the angle is within a suitable range
  var boundedAngle = gr.rotation
  if boundedAngle > midPointAngle {
      boundedAngle -= 2.0 * CGFloat(M_PI)
  } else if boundedAngle < (midPointAngle - 2.0 * CGFloat(M_PI)) {
      boundedAngle += 2 * CGFloat(M_PI)
  // 3. Bound the angle to within the suitable range
  boundedAngle = min(self.endAngle, max(self.startAngle, boundedAngle))
  // 4. Convert the angle to a value
  let angleRange = endAngle - startAngle
  let valueRange = maximumValue - minimumValue
  let valueForAngle = Float(boundedAngle - startAngle) / Float(angleRange) * valueRange + minimumValue
  // 5. Set the control to this value
  self.value = valueForAngle

This method looks quite long and complicated, but the concept is pretty simple – it simply extracts the angle from the custom gesture recognizer, converts it to the value represented by this angle on the knob control, and then sets the value to trigger the UI updates.

Going through the commented sections of the code above, you’ll find the following:

  1. First, you calculate the angle which represents the ‘mid-point’ between the start and end angles. This is the angle which is not part of the knob track, and instead represents the angle at which the pointer should flip between the maximum and minimum values.
  2. The angle calculated by the gesture recognizer will be between and π, since it uses the inverse tangent function. However, the angle required for the track should be continuous between the startAngle and the endAngle. Therefore, create a new boundedAngle variable and adjust it to ensure that it remains within the allowed ranges.
  3. Update boundedAngle so that it sits inside the specified bounds of the angles.
  4. Convert the angle to a value, just as you converted it in setValue(_:animated:) before.
  5. Finally, set the knob control’s value to the calculated value.

Build and run your app; play around with your knob control to see the gesture recognizer in action. The pointer will follow your finger as you move it around the control – how cool is that? :]

Sending Action Notifications

As you move the pointer around, you’ll notice that the UISlider doesn’t update. You’ll wire this up using the target-action pattern which is an inherent part of UIControl.

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

knob.addTarget(self, action: "knobValueChanged:", forControlEvents: .ValueChanged)

This is the standard code you’ve used before to add a listener to a UIControl; here you’re listening for value-changed events.

Now actually implement the action method:

func knobValueChanged(knob: Knob) {
  valueSlider.value = knob.value

If the user changes the value on the knob control, then you’ll update the slider and the label.

Build and run your app; move the knob around and…nothing has changed. Whoops. You haven’t actually fired the event from within the knob control itself.

Time to fix that!

Inside the Knob class, add the following code to the end of handleRotation:

// Notify of value change
if continuous {
} else {
  // Only send an update if the gesture has completed
  if (gr.state == UIGestureRecognizerState.Ended) || (gr.state == UIGestureRecognizerState.Cancelled) {

At the beginning of this tutorial you added the continuous property to the API so that the knob control API would resemble that of UISlider. This is the first and only place that you need to use it.

If continuous is set to true, then the event should be fired every time that the gesture sends an update, so call sendActionsForControlEvents:.

If continuous is set to false, then the event should only fire when the gesture ends or is cancelled. Since the control is only concerned with value changes, the only event you need to handle is UIControlEvents.ValueChanged.

Build and run your app again; move the knob around once again and you’ll see the UISlider move to match the value on the knob. Success!

Where to Go From Here?

Your knob control is now fully functional and you can drop it into your apps to enhance their look and feel. However, there are still a lot of ways that you could extend your control:

  • Add extra configuration options to the appearance of the control – perhaps you could allow an image to be used for the pointer.
  • Integrate a label displaying the current value of the control into the center of the knob.
  • Ensure that a user can only interact with the control if their first touch is on the pointer.
  • At the moment, if you resize the knob control, the layers won’t be re-rendered. You can add this functionality with just a few lines of code.

These suggestions are quite good fun, and will help you hone your skills with the different features of iOS you’ve encountered in this custom control tutorial. And the best part is that you can apply what you’ve learned in other controls that you build.

You can download a zip file of the completed project.

I’d love to hear your comments or questions in the forums below!

Custom Control Tutorial for iOS and Swift: A Reusable Knob is a post from: Ray Wenderlich

The post Custom Control Tutorial for iOS and Swift: A Reusable Knob appeared first on Ray Wenderlich.



Write a comment