Think & Build

custom_control

How to build a custom control in iOS

Following the many requests on Reddit, here we go with the first article about custom controls. Thanks for suggesting it!

Whether you design your super custom user interfaces yourself, or a designer does the job for you, UIKit standard controls won’t likely be enough for your needs.

For example, what if you want to build a control to help the user select an Angle value between 0 and 360?
A solution would be to create a circular slider and let the user drag the knob to select the angle value. This is something you’ve probably already seen in many other interfaces but nothing similar is available in UIKit.

That’s why this is the perfect example we can use to set aside UIKit and build something special. But first, here’s a sneak peek of what we are going to create.

Subclassing UIControl

The UIControl class is subclass of UIView and it is the parent of every UIKit control (such as UIButton, UISlider, UISwitch and so on).
The main role of UIControl instances is to create a logic to dispatch actions to their targets, mainly (like 90% of the times) using a specific user interface which draws the control depending on its state (i.e. Highlighted, Selected, Disabled…).

With UIControl we manage three important tasks:

  1. Drawing the user interface
  2. Tracking user’s interaction
  3. the Target-Action pattern

In the Circular Slider we’re going to build:

we define a User Interface (the circular slider itself) the user can interact with (he moves the knob). User’s decisions are then converted into actions for the control’s target (The control converts the knob frame origin in a value from 0 to 360 and it applies the target/action pattern).

We are ready to open XCode. I suggest you to download the full project at the end of this article and follow this tutorial reading my code.

We will go through the three steps that I have listed previously.
These steps are completely modular, it means that if you are not interested in the way I drew the component, you can just skip to steps 2 and 3.

Open the file TBCircluarSlider.m to follow the next sections.

1) Drawing the user Interface

I love Core Graphics and I want to create something that you can customise further in your experiments. The only part that I want to draw with UIKit is the textfield which presents the slider value.

Warning: some Core Graphics knowledge is needed, but you should be able to read the code anyway, I will explain as much as I can along the way.

Let’s analyse the different parts of the control to have a better view of how it’s drawn.

First, a black circle which defines the slider background.

The active area filled with a gradient from blue to violet.

The handle which is dragged by the user to choose the value .

And last, a TextField to indicate the selected angle. In the next version it will also receive values from the keyboard.

To draw the interface we mainly use the drawRect function, where the first action is to get the current graphic context.

    CGContextRef ctx = UIGraphicsGetCurrentContext();

Drawing the Background

The background is defined by a 360° Arc. This can be simply drawn adding the right path to the context CGContextAddArc and adding a stroke to it.

This is the code used to achieve this simple task:

		//Add the arc path
    CGContextAddArc(ctx, self.frame.size.width/2, self.frame.size.height/2, radius, 0, M_PI *2, 0);
	
		//Set the stroke colour
    [[UIColor blackColor]setStroke];

    //set Line width and cap
    CGContextSetLineWidth(ctx, TB_BACKGROUND_WIDTH);
    CGContextSetLineCap(ctx, kCGLineCapButt);
    
    //draw it!
    CGContextDrawPath(ctx, kCGPathStroke);

The function CGContextArc takes the coordinates for the center of the Arc and the radius (a private int variable). Then it needs the start and the end angle expressed in Radians (you can find a list of Math helpers at the top of the file TBCircularSlider.m) and the last parameter is the drawing direction, 0 means counterclockwise.
The other rows are just settings, like colour and line-width. And finally we draw the path using the function CGContextDrawPath.

Drawing the active area

This part is a little tricky. We draw a linear gradient masked by an image. Let’s see how it works.

The mask image works as a hole through which we can see only a portion of the original gradient rectangle.
An interesting aspect is that this time the arc is drawn with a shadow, that creates a mask with a sort of blur effect.

Creating the mask image:

    UIGraphicsBeginImageContext(CGSizeMake(320,320));
    CGContextRef imageCtx = UIGraphicsGetCurrentContext();
    
    CGContextAddArc(imageCtx, self.frame.size.width/2  , self.frame.size.height/2, radius, 0, ToRad(self.angle), 0);
    [[UIColor redColor]set];
    
    //Use shadow to create the Blur effect
    CGContextSetShadowWithColor(imageCtx, CGSizeMake(0, 0), self.angle/20, [UIColor blackColor].CGColor);
    
    //define the path
    CGContextSetLineWidth(imageCtx, TB_LINE_WIDTH);
    CGContextDrawPath(imageCtx, kCGPathStroke);
    
    //save the context content into the image mask
    CGImageRef mask = CGBitmapContextCreateImage(UIGraphicsGetCurrentContext());
    UIGraphicsEndImageContext();

First, we create an image context, then we activate the shadows. The function CGContextSetShadowWithColor helps us choosing:

  • The context
  • Offset value (which we don’t need)
  • Blur value(we parameterised this value using the current angle divided by twenty to obtain a simple animation on the blurred area during user interaction)
  • The colour

Again we draw an arc, this time depending on the current angle.

For example if the instance variable angle is equal to 360° we draw a full arc, whereas if it is 90° we only draw a portion of it. At the end we obtain an image from the current drawing using the function CGBitmapContextCreateImage. This image will be the mask.

Clipping the Context:

Now that we have the mask we can define “the hole” through which we’ll see the gradient.

We clip the context using the function CGContextClipToMask and passing to the function the mask we have just created.

   
    CGContextClipToMask(ctx, self.bounds, mask);

And finally we can draw the gradient:

    //Define the colour steps
    CGFloat components[8] = {
        0.0, 0.0, 1.0, 1.0,     // Start color - Blue
        1.0, 0.0, 1.0, 1.0 };   // End color - Violet
    
    CGColorSpaceRef baseSpace = CGColorSpaceCreateDeviceRGB();
    CGGradientRef gradient = CGGradientCreateWithColorComponents(baseSpace, components, NULL, 2);
  
    //Define the gradient direction 
    CGPoint startPoint = CGPointMake(CGRectGetMidX(rect), CGRectGetMinY(rect));
    CGPoint endPoint = CGPointMake(CGRectGetMidX(rect), CGRectGetMaxY(rect));

		//Choose a colour space
    CGColorSpaceRelease(baseSpace), baseSpace = NULL;    

    //Create and Draw the gradient
    CGContextDrawLinearGradient(ctx, gradient, startPoint, endPoint, 0);
    CGGradientRelease(gradient), gradient = NULL;

Drawing the gradient is quite a long process, but it’s basically divided in 4 parts (just like the comments in the code):

  1. Defining the colour steps
  2. Defining the gradient direction
  3. Choosing a colour space
  4. Creating and Drawing the gradient

Thanks to the mask only a part of this gradient rectangle will be visible.

As a plus, I added some light effects to simulate light reflections on the background borders (this is optional… you can see how to do that taking a look at the project code).

Drawing the Handle

Now we want to draw the handle at the right position for the current angle.
This step is really simple in terms of drawing (we just draw a white circle), but it needs some calculations to obtain the handle position.
We have to convert a scalar number into a CGPoint using trigonometry. Fear not, it’s just a matter of using Sin and Cos prebuilt functions.

-(CGPoint)pointFromAngle:(int)angleInt{
    
    //Define the Circle center
    CGPoint centerPoint = CGPointMake(self.frame.size.width/2 - TB_LINE_WIDTH/2, self.frame.size.height/2 - TB_LINE_WIDTH/2);
    
    //Define The point position on the circumference
    CGPoint result;
    result.y = round(centerPoint.y + radius * sin(ToRad(-angleInt))) ;
    result.x = round(centerPoint.x + radius * cos(ToRad(-angleInt)));
    
    return result;
}

Given an angle, to find a point on a circumference, we also need the center of the circumference and its radius.

Using the sin function we obtain the value for the Y coordinate and using the cos function the value for the X coordinate.

Remember that each function returns a value for an hypothetic radius of 1. We just have to multiply these results for our radius and move those in relation to the centre of the circumference.

I hope this formula will help you better understand:

  
point.y = center.y + (radius * sin(angle)); 
point.x = center.x + (radius * cos(angle));

Now that we know how to get the knob position, we can draw it using the function we have just built:

-(void) drawTheHandle:(CGContextRef)ctx{
    
    CGContextSaveGState(ctx);
    
    //I Love shadows
    CGContextSetShadowWithColor(ctx, CGSizeMake(0, 0), 3, [UIColor blackColor].CGColor);
    
    //Get the handle position!
    CGPoint handleCenter =  [self pointFromAngle: self.angle];
    
    //Draw It!
    [[UIColor colorWithWhite:1.0 alpha:0.7]set];
    CGContextFillEllipseInRect(ctx, CGRectMake(handleCenter.x, handleCenter.y, TB_LINE_WIDTH, TB_LINE_WIDTH));
    
    CGContextRestoreGState(ctx);
}

The steps now are:

  • Saving the current context (it is a good practice to save the context state when you do drawing actions in a separate function).
  • Setting some shadows for the knob.
  • Defining the knob colour and draw it using the function CGContextFillEllipseInRect.

We call this function at the end of the drawRect function.

    [self drawTheHandle:ctx];

We are done with the drawing part.

2) Tracking user’s interaction

Subclassing the UIControl class, we can override 3 special methods providing a custom tracking behaviour.

Begin tracking
When a touch event happens in the control bounds, the method beginTrackingWithTouch is first sent to the control.

Let’s see how to override it:

 
-(BOOL)beginTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)event{
    [super beginTrackingWithTouch:touch withEvent:event];

    //We need to track continuously
    return YES;
}

It returns a BOOL which determines if the control needs to responds when touch is dragged. In our case we need to track dragging, so we return YES.
This method takes 2 params, the touch object and the event.

Continuing the tracking

In the previous method we have specified that we want to track a continuous event, so a specific method, continueTrackingWithTouch, will be fired when the user performs drag[ging]:

-(BOOL)continueTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)event

This method return a BOOL indicating if touch tracking should continue or not.

We can use this method to filter user’s actions depending on the touch locations. For example, we can choose to activate the control only if the touch location intersects the handle position. Not the case of this control, though, because we want to move the handle in response of any touch position.

For this tutorial this method is responsible for changing the handle’s position (and as we’ll see in the next section it sends the action to the target).

We override it with this code:

-(BOOL)continueTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)event{
    [super continueTrackingWithTouch:touch withEvent:event];

    //Get touch location
    CGPoint lastPoint = [touch locationInView:self];

    //Use the location to design the Handle
    [self movehandle:lastPoint];

 		//We'll see this function in the next section:
    [self sendActionsForControlEvents:UIControlEventValueChanged];

    return YES;
}

At first we take the touch position using locationInView. Then we pass it to the function moveHandle which converts the value to a valid handle position.

What I mean with “a valid position”?
The handle should be moved only within the circumference boundaries defined by the background arc. But we don’t want to force our user to move his finger within this little space to be able to move the handle. The experience would be really frustrating. So we’ll accept any touch position and translate it to the slider’s circumference.
The function moveHandle does the job, plus, in this function, we perform the conversion which gives us the angular value of the slider.

-(void)movehandle:(CGPoint)lastPoint{
    
    //Get the center
    CGPoint centerPoint = CGPointMake(self.frame.size.width/2,
																			self.frame.size.height/2);
    
    //Calculate the direction from the center point to an arbitrary position.
    float currentAngle = AngleFromNorth(centerPoint, 
																				lastPoint, 
																				NO);
    int angleInt = floor(currentAngle);
    
    //Store the new angle
    self.angle = 360 - angleInt;

    //Update the textfield 
    _textField.text =  [NSString stringWithFormat:@"%d", 
																									self.angle];
    
    //Redraw
    [self setNeedsDisplay];
} 

Most of the work is done by AngleFromNorth. Given 2 points, it returns the angle of the imaginary line to connect them.

static inline float AngleFromNorth(CGPoint p1, CGPoint p2, BOOL flipped) {
    CGPoint v = CGPointMake(p2.x-p1.x,p2.y-p1.y);
    float vmag = sqrt(SQR(v.x) + SQR(v.y)), result = 0;
    v.x /= vmag;
    v.y /= vmag;
    double radians = atan2(v.y,v.x);
    result = ToDeg(radians);
    return (result >=0  ? result : result + 360.0);
}

(Note: I’m not the author of angleFromNorth, I took it directly from an example by Apple for OSX called clockControl).

Now that we have the value expressed in degrees, we store it in the property angle and we update the textfield value.

The function setNeedDisplay ensures that the method drawRect will be called, as soon as possible, with these new values.

End tracking

This is the function fired when tracking ends.

-(void)endTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)event{
    [super endTrackingWithTouch:touch withEvent:event];
}

For this example we don’t need to override this function, but it would be useful if we needed to perform operations when the user finishes interacting with the control.

3) Target-Action pattern

At this point the Circular Slider is working. You can drag the knob and see the value changing in the textfield. Now.

Sending actions for control events

If we want to be consistent with the UIControls’ behaviour we have to notify when the control value changes. To do that we use the function sendActionsForControlEvents specifying the event type, in this case UIControlEventValueChanged.

There is quite a long list of possible values (cmd+mouse click on UIControlEventValueChanged from Xcode to see the list). For example, if your control is a subclass of UITextField, you will be interested in UIControlEventEdigitingDidBegin, or, if you want to notify a touch Up action, you can use UIControlTouchUpInside.

If you look back at section 2 you’ll see that we call
sendActionsForControlEvents just before the return within function continueTrackingWithTouch.

    [self sendActionsForControlEvents:UIControlEventValueChanged];

Thanks to that, when the user moves the handle changing the slider value, each registered object will be notified about this change.

How to use the Control

We have now a custom control that we can use in our application.

Open the file TBViewController.m to follow this part and check the viewDidLoad function:


- (void)viewDidLoad
{
    [super viewDidLoad];
    self.view.backgroundColor = [UIColor colorWithRed:0.1 green:0.1 blue:0.1 alpha:1];
    
    //Create the Circular Slider
    TBCircularSlider *slider = [[TBCircularSlider alloc]initWithFrame:CGRectMake(0, 60, TB_SLIDER_SIZE, TB_SLIDER_SIZE)];
    
    //Define Target-Action behaviour
    [slider addTarget:self action:@selector(newValue:) forControlEvents:UIControlEventValueChanged];
    
    [self.view addSubview:slider];
}

We simply define a backgroundcolor and we instantiate a circular slider object adding the frame information calling initWithFrame.

Nothing more than what we do with a UIView (remember: UIControl is subclass of UIView, so it inherits every UIView method).

We can now define how we want to interact with the control, using the method addTarget:action:forControlEvent:

This method just sets the target-action pattern for a specific control’s event. If you remember, the circular slider sends a UIControlEventValueChanged every time the user moves the handle. So we can register an action for this event using the code:

   [slider addTarget:self action:@selector(newValue:) forControlEvents:UIControlEventValueChanged]; 

And we can build the function **newValue** to do something with the Value-Changed information:

    -(void)newValue:(TBCircularSlider*)slider{
        NSLog(@"Slider Value %d",slider.angle);
    }

We are working with the Target-Action pattern, so the function receives the sender which has called the action, in this case the slider. We can directly access the angle value and, for this simple example, just log it.

Conclusions

You can create WHATEVER YOU WANT, starting from the steps I have shown you in this tutorial.
There are probably many other ways to build something like this, but I’ve tried to follow Apple’s suggestions, showing you only the 100% “documented way”.

Any questions, suggestions or if you want to share your ideas on custom controllers, ping me on twitter :)

Download Source