How To Make a Custom Control

In this tutorial, you will implement your very own custom control. You’ll touch on such concepts as extending existing controls, designing and implementing your control’s API, and even how to share your new control with the development community. By Colin Eberhardt.

Leave a rating/review
Save for later
Share
You are currently viewing page 2 of 3 of this article. Click here to view the first page.

Adding Default Control Properties

Open up CERangeSlider.h and add the following properties between the @interface / @end statements:

@property (nonatomic) float maximumValue;
@property (nonatomic) float minimumValue;
@property (nonatomic) float upperValue;
@property (nonatomic) float lowerValue;

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!

Open up CERangeSlider.m, locate initWithFrame which Xcode generated for you, and replace it with the following code:

- (id)initWithFrame:(CGRect)frame
{
    self = [super initWithFrame:frame];
    if (self) {
        // Initialization code
        _maximumValue = 10.0;
        _minimumValue = 0.0;
        _upperValue = 8.0;
        _lowerValue = 2.0;
    }
    return self;
}

Now it’s time to work on the interactive elements of your control; namely, the knobs to represent the high and low values, and the track the knobs 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 can 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 CoreGraphics 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 colours, border thickness, and curvature — pretty much every visual element that goes into drawing your control! This approach allows developers who use your control to easily tailor it to their needs.

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

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 customisation. After all, they want all apps to end up with a similar look-and-feel.

In Xcode, click on the project root to bring up the project settings page. Next, select the Build Phases tab and expand the Link Binary With Libraries section. Now click the plus (+) button at the bottom left of that section you just opened.

Either search for, or look down the list for, QuartzCore.framework. Select QuartzCore.framework and then click Add.

The reason you need to add the QuartzCore framework is because you’ll be using classes and methods from it to do manual drawing of the control.

This screenshot should help you find your way to adding the QuartzCore framework if you’re struggling:

Open up CERangeSlider.m and add the following import to the top of the file.

#import <QuartzCore/QuartzCore.h>

Add the following instance variables to CERangeSlider.m, just after the @implementation statement:

@implementation CERangeSlider
{
    CALayer* _trackLayer;
    CALayer* _upperKnobLayer;
    CALayer* _lowerKnobLayer;
    
    float _knobWidth;
    float _useableTrackLength;
}

These three layers — _tracklayer, _upperKnobLayer, and _lowerKnobLayer — will be used to render the various components of your slider control. The two variables _knobWidth and _useableTrackLength will be used for layout purposes.

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

In CERangeSlider.m, locate initWithFrame: and add the following code just below the code you added to initialise the instance variables, inside the if (self) { } block:

_trackLayer = [CALayer layer];
_trackLayer.backgroundColor = [UIColor blueColor].CGColor;
[self.layer addSublayer:_trackLayer];
        
_upperKnobLayer = [CALayer layer];
_upperKnobLayer.backgroundColor = [UIColor greenColor].CGColor;
[self.layer addSublayer:_upperKnobLayer];
        
_lowerKnobLayer = [CALayer layer];
_lowerKnobLayer.backgroundColor = [UIColor greenColor].CGColor;
[self.layer addSublayer:_lowerKnobLayer];
                                           
[self setLayerFrames];

The above code simply creates three layers and adds them as children of the control’s root layer.

Next, add the following methods to CERangeSlider.m:

- (void) setLayerFrames
{
    _trackLayer.frame = CGRectInset(self.bounds, 0, self.bounds.size.height / 3.5);
    [_trackLayer setNeedsDisplay];

    _knobWidth = self.bounds.size.height;
    _useableTrackLength = self.bounds.size.width - _knobWidth;

    float upperKnobCentre = [self positionForValue:_upperValue];
    _upperKnobLayer.frame = CGRectMake(upperKnobCentre - _knobWidth / 2, 0, _knobWidth, _knobWidth);

    float lowerKnobCentre = [self positionForValue:_lowerValue];
    _lowerKnobLayer.frame = CGRectMake(lowerKnobCentre - _knobWidth / 2, 0, _knobWidth, _knobWidth);

    [_upperKnobLayer setNeedsDisplay];
    [_lowerKnobLayer setNeedsDisplay];
}
                                           
- (float) positionForValue:(float)value
{
    return _useableTrackLength * (value - _minimumValue) /
        (_maximumValue - _minimumValue) + (_knobWidth / 2);
}

setLayerFrames sets the frame for both knob layers and the track layer based on the current slider values. 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.

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

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 knob 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 knob is being dragged, and reflect that in the UI. The control’s layers are a great place to put this logic.

Right-click the CERangeSlider group in the Project Navigator and select New File…. Next, select the iOS\Cocoa Touch\Objective-C class template and add a class called CERangeSliderKnobLayer, making it a subclass of CALayer.

Open up the newly added header CERangeSliderKnobLayer.h and replace its contents with the following:

#import <QuartzCore/QuartzCore.h>

@class CERangeSlider;

@interface CERangeSliderKnobLayer : CALayer

@property BOOL highlighted;
@property (weak) CERangeSlider* slider;

@end

This simply adds two properties, one that indicates whether this knob is highlighted, and one that is a reference back to the parent range slider.

Colin Eberhardt

Contributors

Colin Eberhardt

Author

Over 300 content creators. Join our team.