Using CAShapeLayer to Create a Simple Indicator (Objective-C)

Torrey Betts / Tuesday, October 15, 2013

Introduction

CALayers are extremely powerful for rendering primitives and geometry, then animating out changes to the geometry or visual properties such as color. In this blog post we'll create a flat indictator view using a CAShapeLayer. With very little code you'll have a control that's great for visualizing on and off states. The illustration below demonstrates the example project when ran. When the indicator has been tapped by a user, the indicator changes state and color.

Introduction

Creating the Indicator View

The first step to create our indicator view is to create a class that derives from UIView. In the header file for this new class, we'll add an exposed property that determines the on/off state.

@interface CAIndicatorView : UIView

@property (nonatomic, assign) BOOL on;

@end

In the implementation file (.m) create a category for the class' private properties.

@interface CAIndicatorView()
{
    CAShapeLayer *_circle;
    UITapGestureRecognizer *_tapGestureRecognizer;
    BOOL _hasInitialized;
}
@end

Next, in the implementation for the indicator view we'll override initWithFrame: and also the setFrame: method. These methods will allow us to initialize the control once a frame not equal to CGRectZero has been set. If the control has previously been initialized but the frame is being set, the indicator path and position are updated.

- (void)setFrame:(CGRect)frame
{
    [super setFrame:frame];

    if (!_hasInitialized && !CGRectEqualToRect(frame, CGRectZero))
        [self initializeControl];
    else if (_hasInitialized && !CGRectEqualToRect(frame, CGRectZero))
    {
        int radius = self.bounds.size.width / 2;
        _circle.path = [self generateCirclePathWithRadius:radius];
        _circle.position = CGPointMake(CGRectGetMidX(self.bounds) - radius, CGRectGetMidY(self.bounds) - radius);
    }
}

- (id)initWithFrame:(CGRect)frame
{
    self = [super initWithFrame:frame];
    if (self)
    {
        if (!_hasInitialized && !CGRectEqualToRect(frame, CGRectZero))
            [self initializeControl];
    }

    return self;
}

To initialize the control, we'll create a method called initalizeControl that creates a CAShapeLayer that uses a circle path to represent our indicator. A gesture recognizer is also added to the control for toggling the on/off state. It's important to note that userInteractionEnabled must be set to YES for the UIView to receive the tap.

-(void)initializeControl
{
    int radius = self.bounds.size.width / 2;

    _circle = [CAShapeLayer layer];
    _circle.path = [self generateCirclePathWithRadius:radius];
    _circle.position = CGPointMake(CGRectGetMidX(self.bounds) - radius, CGRectGetMidY(self.bounds) - radius);
    _circle.fillColor = [UIColor redColor].CGColor;
    _circle.strokeColor = [UIColor colorWithRed:0.75 green:0.0 blue:0.0 alpha:1.0].CGColor;
    _circle.lineWidth = self.bounds.size.width * 0.05;
    [self.layer addSublayer:_circle];

    _tapGestureRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(toggleOnOff)];
    [self addGestureRecognizer:_tapGestureRecognizer];

    self.userInteractionEnabled = YES;
    _hasInitialized = YES;
}

-(CGPathRef)generateCirclePathWithRadius:(CGFloat)radius
{
    return [UIBezierPath bezierPathWithRoundedRect:CGRectMake(0, 0, 2.0 * radius, 2.0 * radius) cornerRadius:radius].CGPath;
}

-(void)toggleOnOff
{
    self.on = !self.on;
}

To finish the indicator we need to override the setOn: property setter. Setting this property animates into the next state color. This indicator uses a green color for the on state and red for the off state. When the strokeColor and fillColorproperties the CAShapeLayers animate to these to the next value automatically, so no extra code is necessary.

- (void)setOn:(BOOL)on
{
    _on = on;
    if (_on)
    {
        _circle.fillColor = [UIColor greenColor].CGColor;
        _circle.strokeColor = [UIColor colorWithRed:0.0 green:0.75 blue:0.0 alpha:1.0].CGColor;
    }
    else
    {
        _circle.fillColor = [UIColor redColor].CGColor;
        _circle.strokeColor = [UIColor colorWithRed:0.75 green:0.0 blue:0.0 alpha:1.0].CGColor;
    }
}

Implementing the Indicator View

With the indicator control now complete, we can implement it in our view controller. In this example, we're placing the indicator directly in the center of the view controller. If you choose to use this indicator in other projects, any size or placement would work without altering indicator's code.

_indicatorView = [[CAIndicatorView alloc] init];
_indicatorView.autoresizingMask = UIViewAutoresizingFlexibleBottomMargin|UIViewAutoresizingFlexibleLeftMargin|UIViewAutoresizingFlexibleRightMargin|UIViewAutoresizingFlexibleTopMargin;
_indicatorView.frame = CGRectMake(0, 0, 150, 150);
_indicatorView.center = self.view.center;
[self.view addSubview:_indicatorView];

Download the Example Project

The Xcode project source code for this quick tip can be downloaded by clicking this link.

By Torrey Betts