2014-12-20

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:

Now replace viewDidLoad with the following code:

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:

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:

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:

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:

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:

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:

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:

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:

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:

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:

Then add the following method, also to Knob:

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:

Your initializer should now look like this:

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:

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:

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:

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:

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:

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:

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:

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:

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:

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:

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:

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:

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:

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:

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

Show more