How To Make a Custom Control Tutorial: A Reusable Knob
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. This custom control tutorial covers the creation of a control kind of like a circular slider inspired by a control knob, such as those found on a mixer. By Lorenzo Boaro.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
How To Make a Custom Control Tutorial: A Reusable Knob
30 mins
- Getting Started
- Designing Your Control’s API
- Setting the Appearance of Your Control
- Exposing Appearance Properties in the API
- Setting the Control’s Value Programmatically
- Animating Changes to the Control’s Value
- Updating the Label
- Responding to Touch Interaction
- Wiring Up the Custom Gesture Recognizer
- Sending Action Notifications
- Where to Go From Here?
Updating the Label
Next you’ll populate the label to the right of the knob with its current value. Open ViewController.swift and add this method below the two @IBAction
methods:
func updateLabel() {
valueLabel.text = String(format: "%.2f", knob.value)
}
This will show the current value selected by the knob control. Next, call this new method at the end of both handleValueChanged(_:)
and handleRandomButtonPressed(_:)
like this:
updateLabel()
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.setValue(valueSlider.value)
updateLabel()
Build and run, and perform a few tests to make sure the label shows the correct value.
Responding to Touch Interaction
The knob control you’ve built responds only 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.
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 your control.
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
.
The import
is necessary so you can override some gesture recognizer methods later.
Add the following property to your RotationGestureRecognizer
class:
private(set) var touchAngle: CGFloat = 0
touchAngle
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: Set<UITouch>, with event: UIEvent) {
super.touchesBegan(touches, with: event)
updateAngle(with: touches)
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent) {
super.touchesMoved(touches, with: event)
updateAngle(with: touches)
}
Both of these methods call through to their super
equivalent, and then call a utility function which you’ll add next:
private func updateAngle(with touches: Set<UITouch>) {
guard
let touch = touches.first,
let view = view
else {
return
}
let touchPoint = touch.location(in: view)
touchAngle = angle(for: touchPoint, in: view)
}
private func angle(for point: CGPoint, in view: UIView) -> CGFloat {
let centerOffset = CGPoint(x: point.x - view.bounds.midX, y: point.y - view.bounds.midY)
return atan2(centerOffset.y, centerOffset.x)
}
updateAngle(with:)
takes the set of touches and extracts the first one. It then uses location(in:)
to translate the touch point into the coordinate system of the view associated with this gesture recognizer. It then updates the touchAngle
property using angle(for:in:)
, which uses some simple geometry to 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
. To calculate touchAngle
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
angle(for:in:)
performs this calculation for you, and returns the angle required.
Finally, your gesture recognizer should work with one touch at a time. Add the following initializer to the class:
override init(target: Any?, action: Selector?) {
super.init(target: target, action: action)
maximumNumberOfTouches = 1
minimumNumberOfTouches = 1
}
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.
In Knob
, add the following to the end of commonInit()
:
let gestureRecognizer = RotationGestureRecognizer(target: self, action: #selector(Knob.handleGesture(_:)))
addGestureRecognizer(gestureRecognizer)
This creates a recognizer, specifies it should call Knob.handleGesture(_:)
when activated, then adds it to the view. Now you need to implement that action!
Add the following method to Knob
:
@objc private func handleGesture(_ gesture: RotationGestureRecognizer) {
// 1
let midPointAngle = (2 * CGFloat(Double.pi) + startAngle - endAngle) / 2 + endAngle
// 2
var boundedAngle = gesture.touchAngle
if boundedAngle > midPointAngle {
boundedAngle -= 2 * CGFloat(Double.pi)
} else if boundedAngle < (midPointAngle - 2 * CGFloat(Double.pi)) {
boundedAngle -= 2 * CGFloat(Double.pi)
}
// 3
boundedAngle = min(endAngle, max(startAngle, boundedAngle))
// 4
let angleRange = endAngle - startAngle
let valueRange = maximumValue - minimumValue
let angleValue = Float(boundedAngle - startAngle) / Float(angleRange) * valueRange + minimumValue
// 5
setValue(angleValue)
}
This method 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.
Here’s what happening in the code above:
- 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.
- 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 theendAngle
. Therefore, create a newboundedAngle
variable and adjust it to ensure that it remains within the allowed ranges. - Update
boundedAngle
so that it sits inside the specified bounds of the angles. - Convert the angle to a value, just as you converted it in
setValue(_:animated:)
earlier. - 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 :]