The first step to create our switch 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 CASwitchView : UIView @property (nonatomic, assign) BOOL on; @end

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

@interface CASwitchView () { CAShapeLayer *_track; 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)) { [self adjustControl]; } } - (id)initWithFrame:(CGRect)frame { self = [super initWithFrame:frame]; if (self) { if (!_hasInitialized && !CGRectEqualToRect(frame, CGRectZero)) [self initializeControl]; } return self; }

In setFrame: we need to adjust the geometry of the switch layers since they are not automatically fitted like when using the autoResizingMask of a UIView. To do this, we create a adjustControl method to update the path and position of the track and circle layers.

-(void)adjustControl { _track.path = [self generateTrackPath]; CGRect trackRect = CGPathGetBoundingBox(_track.path); int radius = trackRect.size.width / 3.35; _circle.path = [self generateCirclePathWithRadius:radius trackRect:trackRect]; if (_circle.position.x == 5) { _circle.position = CGPointMake(5, CGRectGetMidY(self.bounds) - (trackRect.size.height / 2)); } else { CGRect circleRect = CGPathGetBoundingBox(_circle.path); _circle.position = CGPointMake(self.bounds.size.width - (circleRect.size.width + 5), CGRectGetMidY(self.bounds) - (trackRect.size.height / 2)); } }

To initialize the control, we'll create a method called initalizeControl that creates a CAShapeLayer that uses a rounded rectangle to represent the track of our switch, and 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 { _track = [CAShapeLayer layer]; _track.path = [self generateTrackPath]; _track.fillColor = [UIColor colorWithWhite:0.95 alpha:1.0].CGColor; _track.strokeColor = [UIColor colorWithWhite:0.4 alpha:1.0].CGColor; _track.lineWidth = 1; [self.layer addSublayer:_track]; CGRect trackRect = CGPathGetBoundingBox(_track.path); int radius = trackRect.size.width / 3.35; _circle = [CAShapeLayer layer]; _circle.path = [self generateCirclePathWithRadius:radius trackRect:trackRect]; _circle.position = CGPointMake(5, CGRectGetMidY(self.bounds) - (trackRect.size.height / 2)); _circle.fillColor = [UIColor colorWithWhite:0.99 alpha:1.0].CGColor; _circle.strokeColor = [UIColor colorWithWhite:0.5 alpha:1.0].CGColor; _circle.lineWidth = 1.0; _circle.shadowOffset = CGSizeMake(0, 2); _circle.shadowOpacity = 0.5; [self.layer addSublayer:_circle]; _tapGestureRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(toggleOnOff)]; [self addGestureRecognizer:_tapGestureRecognizer]; self.userInteractionEnabled = YES; _hasInitialized = YES; } -(CGPathRef)generateCirclePathWithRadius:(CGFloat)radius trackRect:(CGRect)trackRect { return [UIBezierPath bezierPathWithRoundedRect:CGRectMake(0, 0, 2.0 * radius, trackRect.size.height) cornerRadius:radius].CGPath; } -(CGPathRef)generateTrackPath { CGRect pathRect = CGRectInset(self.bounds, 10, 20); UIBezierPath *bezierPath = [UIBezierPath bezierPathWithRoundedRect:pathRect cornerRadius:self.bounds.size.width / 4]; return bezierPath.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 and repositions the switch to the on/off position. This switch uses a green color for the on state and off white for the off state. When the position and fillColor properties the CAShapeLayers animate to these to the next value automatically, so no extra code is necessary.

- (void)setOn:(BOOL)on { _on = on; CGRect trackRect = CGPathGetBoundingBox(_track.path); CGRect circleRect = CGPathGetBoundingBox(_circle.path); if (_on) { _circle.position = CGPointMake(self.bounds.size.width - (circleRect.size.width + 5), CGRectGetMidY(self.bounds) - (trackRect.size.height / 2)); _track.fillColor = [UIColor greenColor].CGColor; } else { _circle.position = CGPointMake(5, CGRectGetMidY(self.bounds) - (trackRect.size.height / 2)); _track.fillColor = [UIColor colorWithWhite:0.95 alpha:1.0].CGColor; } }