What actually goes into creating a custom control? This article is meant to answer some of that question by providing a walkthrough for creating a simple iOS control. We'll focus on creating a custom iOS checkbox control, and demonstrate the necessary steps to make the control usable in the Xcode designer and your application. checkboxcover

Getting Started

We can first create a new Cocoa Touch Class to represent our control. uicontrolsubclass We'll name it Checkbox and set it as a subclass of UIControl. cocoatouchclass

Creating the Control Properties

A checkbox control is something with a relatively limited scope with regards to its implementation. Unlike a more complicated control such as FlexGrid or FlexChart, we'll only have to deal with relatively narrow functionality that handles being checked, disabled, and styled. Properties for checked and disabled can be simple boolean values, but the styling would require some more thought. We should provide some possibility to customize the color of the control, as well as consider adding some kind of attached label with it's own styling. Since users might not always be interested in having some text directly attached to the label we can provide another boolean to toggle this behavior. The header for the control should look as follows:


@interface Checkbox : UIControl  

-(void)setChecked:(BOOL)isChecked;  
-(void)setEnabled:(BOOL)isEnabled;  
-(void)setText:(NSString *)stringValue;  

@property IBInspectable UIColor *checkColor;  
@property IBInspectable UIColor *boxFillColor;  
@property IBInspectable UIColor *boxBorderColor;  
@property IBInspectable UIFont *labelFont;  
@property IBInspectable UIColor *labelTextColor;  

@property IBInspectable BOOL isEnabled;  
@property IBInspectable BOOL isChecked;  
@property IBInspectable BOOL showTextLabel;  
@property (nonatomic, strong) IBInspectable  NSString *text;  
@end  


You'll notice the recurrence of the IBInspectable on all of the properties I'm defining in the header. An IBInspectable property is visible in Xcode's designer (with a few exceptions). Beginning with Xcode 6, this functionality has become available to allow your custom control's properties to be set and manipulated in the designer. DesignerProperties

The Checkbox Implementation

The implementation for the checkbox control has some nuance, and I'll try to break it down into a few pieces:

  • The first part of our implementation will handle initialization and setting some defaults for the control properties.
  • We'll override both the initWithFrame and initWithCoder methods so that they both call our initInternals method, where we can set defaults for the control properties.
  • We'll also implement the intrinsicContentSize method so that the control has a default size (which comes into play if we place some constraints on it in the designer).

#import "Checkbox.h"  
IB_DESIGNABLE  
@implementation Checkbox{  
    UILabel *label;  
    BOOL textIsSet;  
}  
@synthesize text = _text;  
- (id)initWithCoder:(NSCoder *)aDecoder{  
    self = [super initWithCoder:aDecoder];  
    if (self != nil) {  
        [self initInternals];  
    }  
    return self;  
}  

- (id)initWithFrame:(CGRect)frame{  
    self = [super initWithFrame:frame];  
    if (self != nil) {  
        [self initInternals];  
    }  
    return self;  
}  
- (void) initInternals{  
    _boxFillColor = [UIColor colorWithRed:0 green:.478 blue:1 alpha:1];  
    _boxBorderColor = [UIColor colorWithRed:0 green:.478 blue:1 alpha:1];  
    _checkColor = [UIColor whiteColor];  
    _isChecked = YES;  
    _isEnabled = YES;  
    _showTextLabel = NO;  
    textIsSet = NO;  
    self.backgroundColor = [UIColor clearColor];  
}  
-(CGSize)intrinsicContentSize{  
    if (_showTextLabel) {  
        return CGSizeMake(160, 40);  
    }  
    else{  
        return CGSizeMake(40, 40);  
    }  

}  

We'll also use the IB_DESIGNABLE flag at the top of the implementation file so that we can see the control in the designer. This is another feature that was introduced in Xcode 6, and we'll definitely take advantage of it here. Once we've finished, we'll be able to completely manipulate the control in the designer. checkboxInDesigner The drawRect method is where our control is actually drawn to the screen. This will end up being a fairly complicated method since there are a number of variables that will influence how the checkbox is drawn so we'll break it up into a few parts. We'll start with an empty declaration of drawRect which we'll gradually fill in with the elements of the checkbox.


- (void)drawRect:(CGRect)rect {  
     //Our code for drawing the control goes here  
}  

Elements of a CheckBox Control

Visually the checkbox control is really just a handful of individual elements:

  • Box
  • Check
  • Label
  • Style Properties
  • Interaction

We'll go through how to implement each part in drawRect and provide the full method at the end.

Create the Box

First, let's look at the rectangular box that will be filled with one color, and allow a second color to be set for its border. We can use a UIBezierPath to draw our rectangle. By filling this path we can color the contents of the box; by using the stroke method, we can color in the perimeter of the rectangle (thus acting as a border).



 UIBezierPath *boxPath = [UIBezierPath bezierPathWithRoundedRect:CGRectMake(2, 2, self.frame.size.width - 4, self.frame.size.height - 4) cornerRadius:self.frame.size.width/5];  
 boxPath.lineWidth = 4;  
 [_boxFillColor setFill];  
 [_boxBorderColor setStroke];  
 [boxPath fill];  
 [boxPath stroke];  


Draw the Check

Drawing the check will also use a UIBezierPath, although it will be done slightly differently. First, we'll need to employ some conditional logic to check whether the isChecked boolean is set to YES/true. If it is, then we can draw the check mark.

  1. Use the moveToPoint method to set our path to the starting point (slightly offset from the top right corner).
  2. Use the addLinetoPoint method to add two lines that represent our check mark.
  3. Once the lines are added, we can set the stroke color and call the stroke method.


 if (_isChecked == YES) {  
            UIBezierPath *checkPath = [UIBezierPath bezierPath];  
            checkPath.lineWidth = 5;  
            [checkPath moveToPoint:CGPointMake(self.frame.size.width * 1/5, self.frame.size.height/5)];  
            [checkPath addLineToPoint:CGPointMake(self.frame.size.width/8, self.frame.size.height * 4/5)];  
            [checkPath addLineToPoint:CGPointMake(self.frame.size.width/20, self.frame.size.height/2)];  
            [_checkColor setStroke];  
            [checkPath stroke];  
        }  

Enable User Interaction

We'll also need to add some code to check the isEnabled value to determine whether we should enable user interaction for the control. We'll adjust the alpha value so that the user has a visual cue that the control is disabled.


    if (_isEnabled == YES) {  
        self.alpha = 1.0f;  
        self.userInteractionEnabled = YES;  
    }  
    else{  
        self.alpha = 0.6f;  
        self.userInteractionEnabled = NO;  
    }  

Display the Label (Or Not)

The full implementation of the drawRect method is more complicated, since there's the possibility of the user enabling and placing text into the control. This is dictated by the showTextLabel value, which can be enabled in the designer or in code. This value toggles whether the control is simply a checkbox, or a checkbox with label hybrid, so it requires some extra logic if that label is to be displayed.


- (void)drawRect:(CGRect)rect {  
    // Drawing code  
    [_boxFillColor setFill];  
    [_boxBorderColor setStroke];  
    //User set flag to draw label  
    if (_showTextLabel == YES) {  
        //check if label has already been created... if not create a new label and set some basic styles  
        if (!textIsSet) {  
            label = [[UILabel alloc] initWithFrame:CGRectMake(self.frame.size.width/4 + 5, 0, self.frame.size.width*3/4 - 5, self.frame.size.height)];  
            label.backgroundColor = [UIColor clearColor];  
            [self addSubview:label];  
            textIsSet = YES;  
        }  
        //style label  
        label.font = _labelFont;  
        label.textColor = _labelTextColor;  
        label.text = self.text;  
        //create enclosing box for checkbox  
        UIBezierPath *boxPath = [UIBezierPath bezierPathWithRoundedRect:CGRectMake(2, 2, self.frame.size.width/4 - 4, self.frame.size.height - 4) cornerRadius:self.frame.size.width/20];  
        boxPath.lineWidth = 4;  
        [boxPath fill];  
        [boxPath stroke];  
        //if control is checked draw checkmark  
        if (_isChecked == YES) {  
            UIBezierPath *checkPath = [UIBezierPath bezierPath];  
            checkPath.lineWidth = 5;  
            [checkPath moveToPoint:CGPointMake(self.frame.size.width * 1/5, self.frame.size.height/5)];  
            [checkPath addLineToPoint:CGPointMake(self.frame.size.width/8, self.frame.size.height * 4/5)];  
            [checkPath addLineToPoint:CGPointMake(self.frame.size.width/20, self.frame.size.height/2)];  
            [_checkColor setStroke];  
            [checkPath stroke];  
        }  
    }  
    //no text label in this scenario  
    else{  
        UIBezierPath *boxPath = [UIBezierPath bezierPathWithRoundedRect:CGRectMake(2, 2, self.frame.size.width - 4, self.frame.size.height - 4) cornerRadius:self.frame.size.width/5];  
        boxPath.lineWidth = 4;  
        [boxPath fill];  
        [boxPath stroke];  
        if (_isChecked == YES) {  
            UIBezierPath *checkPath = [UIBezierPath bezierPath];  
            checkPath.lineWidth = 5;  
            [checkPath moveToPoint:CGPointMake(self.frame.size.width * 4/5, self.frame.size.height/5)];  
            [checkPath addLineToPoint:CGPointMake(self.frame.size.width/2, self.frame.size.height * 4/5)];  
            [checkPath addLineToPoint:CGPointMake(self.frame.size.width/5, self.frame.size.height/2)];  
            [_checkColor setStroke];  
            [checkPath stroke];  
        }  
    }  
    //check if control is enabled...lower alpha if not and disable interaction  
    if (_isEnabled == YES) {  
        self.alpha = 1.0f;  
        self.userInteractionEnabled = YES;  
    }  
    else{  
        self.alpha = 0.6f;  
        self.userInteractionEnabled = NO;  
    }  

    [self setNeedsDisplay];  
}  

Track User Interaction and Update the Control

The last step handles both tracking the user interaction and visually updating the control with property setters. We'll use the beginTrackingWithTouch method to track when the control has been clicked and change the isChecked value accordingly. Using this method (along with subclassing UIControl more generally) allows us to take advantage of the target-action mechanism later, which lets us perform other actions when the checkbox is clicked. We'll cover this in more detail when we talk about using the control in the viewController. The other setter methods all call setNeedsDisplay to update the control after a property value has been changed.


-(BOOL)beginTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)event{  
    [self setChecked:!_isChecked];  
    return true;  
}  

-(void)setChecked:(BOOL)isChecked{  
    _isChecked = isChecked;  
    [self setNeedsDisplay];  
}  
-(void)setEnabled:(BOOL)isEnabled{  
    _isEnabled = isEnabled;  
    [self setNeedsDisplay];  
}  
-(void)setText:(NSString *)stringValue{  
    _text = stringValue;  
    [self setNeedsDisplay];  
}  

Using the Checkbox Control

Now that we've completed our checkbox control, we can either use it in the designer or in code. To use the designer:

  1. Drag a generic view onto the design surface
  2. Configure the custom class in the identity inspector to the Checkbox class we just finished

ControlDesigner Working with the control in code is also quite straightforward.

  • Use the viewDidLoad method to instantiate the Checkbox as you would other built in UI controls
  • Use the add target method to trigger your own custom method to handle UIControlEvents, such as when your control is tapped.

In this sample, I've changed the Checkbox text when this occurs, based on the isChecked value.



- (void)viewDidLoad {  
    [super viewDidLoad];  
    // Do any additional setup after loading the view, typically from a nib.  
    cbox = [[Checkbox alloc] initWithFrame:CGRectMake(100, 100, 250, 50)];  
    cbox.text = @"Checked";  
    cbox.showTextLabel = YES;  
    cbox.labelFont = [UIFont systemFontOfSize:34];  
    [cbox addTarget:self action:@selector(checkAction) forControlEvents:UIControlEventTouchUpInside];  
    [self.view addSubview:cbox];  
}  
- (void) checkAction{  
    if (cbox.isChecked == true) {  
        cbox.text = @"Checked";  
    }  
    else{  
        cbox.text = @"Unchecked";  
    }  
}  


This will result in the following behavior when the project is run: CheckToggle

Custom Controls Continued

This is just the starting point for what you can do with custom controls, but hopefully it helps to illustrate what the process looks like. Controls can very quickly balloon in complexity as requirements change and new features are added. All of the Xuni controls have many more layers of intricacy than what's displayed here, but this gives an idea of where to begin. In the future, we'll further examine how we can turn this control into a framework. We'll also connect this control to Xamarin to further demystify what Xamarin does and how it works with a native control. Also be sure to check our recent article on creating a custom Xamarin.Forms control and keep a look out for new content on mobile development and Xuni.

View the Custom Checkbox sample on GitHub>>