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.
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 Slider
40 mins
Adding Touch Handlers
Open RangeSlider.swift add the following property along with the others:
var previousLocation = CGPoint()
This property will be used to track the touch locations.
How are you going to track the various touch and release events of your control?
UIControl
provides several methods for tracking touches. Subclasses of UIControl
can override these methods in order to add their own interaction logic.
In your custom control, you will override three key methods of UIControl
: beginTrackingWithTouch
, continueTrackingWithTouch
and endTrackingWithTouch
.
Add the following method to RangeSlider.swift:
override func beginTrackingWithTouch(touch: UITouch, withEvent event: UIEvent) -> Bool {
previousLocation = touch.locationInView(self)
// Hit test the thumb layers
if lowerThumbLayer.frame.contains(previousLocation) {
lowerThumbLayer.highlighted = true
} else if upperThumbLayer.frame.contains(previousLocation) {
upperThumbLayer.highlighted = true
}
return lowerThumbLayer.highlighted || upperThumbLayer.highlighted
}
The method above is invoked when the user first touches the control.
First, it translates the touch event into the control’s coordinate space. Next, it checks each thumb layer to see whether the touch was within its frame. The return value for the above method informs the UIControl
superclass whether subsequent touches should be tracked.
Tracking touch events continues if either thumb is highlighted.
Now that you have the initial touch event, you’ll need to handle the events as the user moves their finger across the screen.
Add the following methods to RangeSlider.swift:
func boundValue(value: Double, toLowerValue lowerValue: Double, upperValue: Double) -> Double {
return min(max(value, lowerValue), upperValue)
}
override func continueTrackingWithTouch(touch: UITouch, withEvent event: UIEvent) -> Bool {
let location = touch.locationInView(self)
// 1. Determine by how much the user has dragged
let deltaLocation = Double(location.x - previousLocation.x)
let deltaValue = (maximumValue - minimumValue) * deltaLocation / Double(bounds.width - thumbWidth)
previousLocation = location
// 2. Update the values
if lowerThumbLayer.highlighted {
lowerValue += deltaValue
lowerValue = boundValue(lowerValue, toLowerValue: minimumValue, upperValue: upperValue)
} else if upperThumbLayer.highlighted {
upperValue += deltaValue
upperValue = boundValue(upperValue, toLowerValue: lowerValue, upperValue: maximumValue)
}
// 3. Update the UI
CATransaction.begin()
CATransaction.setDisableActions(true)
updateLayerFrames()
CATransaction.commit()
return true
}
boundValue
will clamp the passed in value so it is within the specified range. Using this helper function is a little easier to read than a nested min
/ max
call.
Here’s a breakdown of continueTrackingWithTouch
, comment by comment:
- First you calculate a delta location, which determines the number of pixels the user’s finger travelled. You then convert it into a scaled delta value based on the minimum and maximum values of the control.
- Here you adjust the upper or lower values based on where the user drags the slider to.
- This section sets the
disabledActions
flag inside aCATransaction
. This ensures that the changes to the frame for each layer are applied immediately, and not animated. Finally,updateLayerFrames
is called to move the thumbs to the correct location.
You’ve coded the dragging of the slider — but you still need to handle the end of the touch and drag events.
Add the following method to RangeSlider.swift:
override func endTrackingWithTouch(touch: UITouch, withEvent event: UIEvent) {
lowerThumbLayer.highlighted = false
upperThumbLayer.highlighted = false
}
The above code simply resets both thumbs to a non-highlighted state.
Build and run your project, and play around with your shiny new slider! You should be able to drag the green thumb points around.
You’ll notice that when the slider is tracking touches, you can drag your finger beyond the bounds of the control, then back within the control without losing your tracking action. This is an important usability feature for small screen devices with low precision pointing devices — or as they’re more commonly known, fingers! :]
Change Notifications
You now have an interactive control that the user can manipulate to set upper and lower bounds. But how do you communicate these change notifications to the calling app so that the app knows the control has new values?
There are a number of different patterns that you could implement to provide change notification: NSNotification
, Key-Value-Observing
(KVO), the delegate
pattern, the target-action
pattern and many others. There are so many choices!
What to do?
If you look at the UIKit
controls, you’ll find they don’t use NSNotification
or encourage the use of KVO, so for consistency with UIKit you can exclude those two options. The other two patterns — delegates and target-action patterns — are used extensively in UIKit.
Here’s a detailed analysis of the delegate and the target-action pattern:
Delegate pattern – With the delegate pattern you provide a protocol which contains a number of methods that are used for a range of notifications. The control has a property, usually named delegate
, which accepts any class that implements this protocol. A classic example of this is UITableView
which provides the UITableViewDelegate
protocol. Note that controls only accept a single delegate instance. A delegate method can take any number of parameters, so you can pass in as much information as you desire to such methods.
Target-action pattern – The target-action pattern is provided by the UIControl
base class. When a change in control state occurs, the target is notified of the action which is described by one of the UIControlEvents
enum values. You can provide multiple targets to control actions and while it is possible to create custom events (see UIControlEventApplicationReserved
) the number of custom events is limited to 4. Control actions do not have the ability to send any information with the event. So they cannot be used to pass extra information when the event is fired.
The key differences between the two patterns are as follows:
- Multicast — the target-action pattern multicasts its change notifications, while the delegate pattern is bound to a single delegate instance.
- Flexibility — you define the protocols yourself in the delegate pattern, meaning you can control exactly how much information you pass. Target-action provides no way to pass extra information and clients would have to look it up themselves after receiving the event.
Your range slider control doesn’t have a large number of state changes or interactions that you need to provide notifications for. The only things that really change are the upper and lower values of the control.
In this situation, the target-action pattern makes perfect sense. This is one of the reasons why you were told to subclass UIControl
right back at the start of this tutorial!
Aha! It’s making sense now! :]
The slider values are updated inside continueTrackingWithTouch:withEvent:
, so this is where you’ll need to add your notification code.
Open up RangeSlider.swift, locate continueTrackingWithTouch
, and add the following just before the “return true
” statement:
sendActionsForControlEvents(.ValueChanged)
That’s all you need to do in order to notify any subscribed targets of the changes!
Now that you have your notification handling in place, you should hook it up to your app.
Open up ViewController.swift and add the following code to the end of viewDidLoad
:
rangeSlider.addTarget(self, action: "rangeSliderValueChanged:", forControlEvents: .ValueChanged)
The above code invokes the rangeSliderValueChanged
method each time the range slider sends the UIControlEventValueChanged
action.
Now add the following method to ViewController.swift:
func rangeSliderValueChanged(rangeSlider: RangeSlider) {
println("Range slider value changed: (\(rangeSlider.lowerValue) \(rangeSlider.upperValue))")
}
This method simply logs the range slider values to the console as proof that your control is sending notifications as planned.
Build and run your app, and move the sliders back and forth. You should see the control’s values in the console, as in the screenshot below:
Range slider value changed: (0.117670682730924 0.390361445783134) Range slider value changed: (0.117670682730924 0.38835341365462) Range slider value changed: (0.117670682730924 0.382329317269078) Range slider value changed: (0.117670682730924 0.380321285140564) Range slider value changed: (0.119678714859438 0.380321285140564) Range slider value changed: (0.121686746987952 0.380321285140564)
You’re probably sick of looking at the multi-colored range slider UI by now. It looks like an angry fruit salad!
It’s time to give the control a much-needed facelift!