PaintCode Tutorial: Bezier Paths
In the third and final part of our PaintCode tutorial series, learn how to create dynamic, movable arrows with curved bezier paths! By Felipe Laso-Marsetti.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
PaintCode Tutorial: Bezier Paths
40 mins
- Getting Started
- Drawing the Bezier Arrow
- Adding Gradients and Colors to the Bezier Arrow
- Adding Handles to the Bezier Arrow
- Adding Frames to the Bezier Arrow
- Adding the Bezier Arrow to Your App
- Hooking up the Bezier Arrow Code
- Adding Touch Handlers for the Bezier Arrow
- Putting Your Bezier Arrows to Work
- Where To Go From Here?
Hooking up the Bezier Arrow Code
Switch back to PaintCode and make sure the code view has the platform set to iOS > Objective-C, the OS version as iOS 5+, origin set to Custom Origin, and memory management as ARC, as illustrated in the screenshot below:
Paste all of the code from PaintCode into drawRect:
in BezierView.m. Your method should now appear as follows:
-(void)drawRect:(CGRect)rect {
// General Declarations
CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
CGContextRef context = UIGraphicsGetCurrentContext();
// Color Declarations
UIColor *fillColor = [UIColor colorWithRed:0.22 green:0.267 blue:0.384 alpha:1];
CGFloat fillColorRGBA[4];
[fillColor getRed:&fillColorRGBA[0] green:&fillColorRGBA[1] blue:&fillColorRGBA[2] alpha:&fillColorRGBA[3]];
UIColor* strokeColor = [UIColor colorWithRed:(fillColorRGBA[0] * 0.3)
green:(fillColorRGBA[1] * 0.3)
blue:(fillColorRGBA[2] * 0.3)
alpha:(fillColorRGBA[3] * 0.3 + 0.7)];
UIColor *fillColor2 = [UIColor colorWithRed:0.549 green:0.627 blue:0.753 alpha:1];
UIColor *shadowColor2 = [UIColor colorWithRed:1 green:1 blue:1 alpha:1];
UIColor *shadowColor3 = [UIColor colorWithRed:0 green:0 blue:0 alpha:0.243];
// Gradient Declarations
NSArray *gradientColors = @[(id)fillColor2.CGColor, (id)fillColor.CGColor];
CGFloat gradientLocations[] = {0, 1};
CGGradientRef gradient = CGGradientCreateWithColors(colorSpace, (__bridge CFArrayRef)gradientColors, gradientLocations);
// Shadow Declarations
UIColor *innerShadow = shadowColor2;
CGSize innerShadowOffset = CGSizeMake(0.1, 1.1);
CGFloat innerShadowBlurRadius = 2;
UIColor *shadow = shadowColor3;
CGSize shadowOffset = CGSizeMake(0.1, 1.1);
CGFloat shadowBlurRadius = 2;
// Frames
CGRect frame = CGRectMake(0, 0, 30, 38);
CGRect frame2 = CGRectMake(170, 0, 30, 38);
// Bezier Drawing
UIBezierPath *bezierPath = [UIBezierPath bezierPath];
[bezierPath moveToPoint:CGPointMake(CGRectGetMinX(frame) + 20, CGRectGetMinY(frame))];
[bezierPath addLineToPoint:CGPointMake(CGRectGetMinX(frame), CGRectGetMinY(frame) + 19)];
[bezierPath addLineToPoint:CGPointMake(CGRectGetMinX(frame) + 20, CGRectGetMinY(frame) + 38)];
[bezierPath addCurveToPoint:CGPointMake(CGRectGetMinX(frame) + 20, CGRectGetMinY(frame) + 25)
controlPoint1:CGPointMake(CGRectGetMinX(frame) + 20, CGRectGetMinY(frame) + 38)
controlPoint2:CGPointMake(CGRectGetMinX(frame) + 20, CGRectGetMinY(frame) + 29.88)];
[bezierPath addCurveToPoint:CGPointMake(CGRectGetMinX(frame2) + 10, CGRectGetMinY(frame2) + 25)
controlPoint1:CGPointMake(CGRectGetMinX(frame) + 50.73, CGRectGetMinY(frame) + 25)
controlPoint2:CGPointMake(CGRectGetMinX(frame2) - 23.76, CGRectGetMinY(frame2) + 25)];
[bezierPath addCurveToPoint:CGPointMake(CGRectGetMinX(frame2) + 10, CGRectGetMinY(frame2) + 38)
controlPoint1:CGPointMake(CGRectGetMinX(frame2) + 10, CGRectGetMinY(frame2) + 30.36)
controlPoint2:CGPointMake(CGRectGetMinX(frame2) + 10, CGRectGetMinY(frame2) + 38)];
[bezierPath addLineToPoint:CGPointMake(CGRectGetMinX(frame2) + 30, CGRectGetMinY(frame2) + 19)];
[bezierPath addLineToPoint:CGPointMake(CGRectGetMinX(frame2) + 10, CGRectGetMinY(frame2))];
[bezierPath addCurveToPoint:CGPointMake(CGRectGetMinX(frame2) + 10, CGRectGetMinY(frame2) + 13)
controlPoint1:CGPointMake(CGRectGetMinX(frame2) + 10, CGRectGetMinY(frame2))
controlPoint2:CGPointMake(CGRectGetMinX(frame2) + 10, CGRectGetMinY(frame2) + 7.35)];
[bezierPath addCurveToPoint:CGPointMake(CGRectGetMinX(frame) + 20, CGRectGetMinY(frame) + 13)
controlPoint1:CGPointMake(CGRectGetMinX(frame2) - 25.01, CGRectGetMinY(frame2) + 13)
controlPoint2:CGPointMake(CGRectGetMinX(frame) + 50.92, CGRectGetMinY(frame) + 13)];
[bezierPath addCurveToPoint:CGPointMake(CGRectGetMinX(frame) + 20, CGRectGetMinY(frame))
controlPoint1:CGPointMake(CGRectGetMinX(frame) + 20, CGRectGetMinY(frame) + 7.39)
controlPoint2:CGPointMake(CGRectGetMinX(frame) + 20, CGRectGetMinY(frame))];
[bezierPath closePath];
CGContextSaveGState(context);
CGContextSetShadowWithColor(context, shadowOffset, shadowBlurRadius, shadow.CGColor);
CGContextBeginTransparencyLayer(context, NULL);
[bezierPath addClip];
CGRect bezierBounds = CGPathGetPathBoundingBox(bezierPath.CGPath);
CGContextDrawLinearGradient(context, gradient,
CGPointMake(CGRectGetMidX(bezierBounds), CGRectGetMinY(bezierBounds)),
CGPointMake(CGRectGetMidX(bezierBounds), CGRectGetMaxY(bezierBounds)),
0);
CGContextEndTransparencyLayer(context);
// Bezier Inner Shadow
CGRect bezierBorderRect = CGRectInset([bezierPath bounds], -innerShadowBlurRadius, -innerShadowBlurRadius);
bezierBorderRect = CGRectOffset(bezierBorderRect, -innerShadowOffset.width, -innerShadowOffset.height);
bezierBorderRect = CGRectInset(CGRectUnion(bezierBorderRect, [bezierPath bounds]), -1, -1);
UIBezierPath *bezierNegativePath = [UIBezierPath bezierPathWithRect:bezierBorderRect];
[bezierNegativePath appendPath: bezierPath];
bezierNegativePath.usesEvenOddFillRule = YES;
CGContextSaveGState(context);
{
CGFloat xOffset = innerShadowOffset.width + round(bezierBorderRect.size.width);
CGFloat yOffset = innerShadowOffset.height;
CGContextSetShadowWithColor(context,
CGSizeMake(xOffset + copysign(0.1, xOffset), yOffset + copysign(0.1, yOffset)),
innerShadowBlurRadius,
innerShadow.CGColor);
[bezierPath addClip];
CGAffineTransform transform = CGAffineTransformMakeTranslation(-round(bezierBorderRect.size.width), 0);
[bezierNegativePath applyTransform:transform];
[[UIColor grayColor] setFill];
[bezierNegativePath fill];
}
CGContextRestoreGState(context);
CGContextRestoreGState(context);
[strokeColor setStroke];
bezierPath.lineWidth = 1;
[bezierPath stroke];
// Cleanup
CGGradientRelease(gradient);
CGColorSpaceRelease(colorSpace);
}
Note: As noted in the previous parts of this tutorial, the above code has been slightly modified to use modern Objective-C notation. Otherwise, the code should be fairly similar to what PaintCode generates except for a few values such as the positioning of the bezier point handles.
Note: As noted in the previous parts of this tutorial, the above code has been slightly modified to use modern Objective-C notation. Otherwise, the code should be fairly similar to what PaintCode generates except for a few values such as the positioning of the bezier point handles.
First, you’ll need a view to display your bezier arrow.
Open BezierViewController.m and add the following code directly below the existing #import
line:
#import "BezierView.h"
Still working in BezierViewController.m, add the following code between the @implementation
and @end
lines:
#pragma mark - View Lifecycle
-(void)viewDidLoad {
[super viewDidLoad];
BezierView *midArrow = [[BezierView alloc] initWithLeftArrowTipPoint:CGPointMake(20, 20)
rightArrowTipPoint:CGPointMake(300, 130)
releaseHandler:nil];
[self.view addSubview:midArrow];
}
The above two bits of code simply create a new BezierView
instance with the specified positions for the arrowheads and a nil
release handler. The nil
release handler means that no action is carried out when the right arrow is dragged and released. You then add the new view as a subview of the main view.
Build and run the project and switch to the Bezier tab. You should see your arrow drawn on the screen as follows:
Hey, there’s your arrow…but wait a minute. You specified the coordinates (20,20) and (300,130) for your arrowheads, and those two points definitely aren’t in the same Y-axis. What’s going on here?
Once again, this comes down to the frames used in drawRect:
for the custom drawing calls; they’re using the original coordinates as defined in PaintCode.
Go back to BezierView.m, and locate the section in drawRect:
that starts with the //Frames
comment. Modify that section of code as follows:
- (void)drawRect:(CGRect)rect
{
...
// Frames
CGRect frame = CGRectMake(_leftArrowTip.x,
_leftArrowTip.y - kArrowFrameHeightHalf,
kArrowFrameWidth,
kArrowFrameHeight);
CGRect frame2 = CGRectMake(_rightArrowTip.x - kArrowFrameWidth,
_rightArrowTip.y - kArrowFrameHeightHalf,
kArrowFrameWidth,
kArrowFrameHeight);
...
}
Instead of the default frames that PaintCode provided, you now have your own frames calculated using the left and right arrowhead positions, as well as the width and height of the arrow frame.
Build and run again and behold the result:
Hey, that looks a lot better — the arrows are now drawn exactly where you wanted them, and the bezier curve is calculated according to the supplied arrowhead coordinates.
Adding Touch Handlers for the Bezier Arrow
Okay, so you can control where the arrowheads sit on the screen from code, but you need to allow the user to drag the arrowhead around.
Just before you go all crazy adding touch methods to your code, add a few supporting elements to your code.
First, add the following property to the class extension in BezierView.m:
@property (assign, nonatomic, readonly) CGRect rightArrowFrame;
Next, add the following custom getter for the new property to BezierView.m:
-(CGRect)rightArrowFrame {
return CGRectMake(_rightArrowTip.x - kArrowFrameWidth,
_rightArrowTip.y - kArrowFrameHeightHalf,
kArrowFrameWidth,
kArrowFrameHeight);
}
This code returns a rectangle containing the size and coordinates of the right arrow frame.
Still working in BezierView.m, update the //Frames
section of drawRect:
to reference the custom getter you created in the previous step:
- (void)drawRect:(CGRect)rect
{
...
// Frames
CGRect frame = CGRectMake(_leftArrowTip.x,
_leftArrowTip.y - kArrowFrameHeightHalf,
kArrowFrameWidth,
kArrowFrameHeight);
CGRect frame2 = self.rightArrowFrame;
...
}
A UIView
subclass on its own is not inherently interactive, but it can instantly respond to touches because it’s also a subclass of UIResponder
. There’s four methods to override in order to detect touches and to make the right arrow draggable:
-(void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
-(void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
-(void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
Add the code below to BezierView.m:
-(void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event {
_touchState = TouchStateInvalid;
}
Since the above method is called when a touch is cancelled, the method simply sets the _touchState
variable to indicate that no dragging is taking place.
Now add the following code to BezierView.m:
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
CGPoint touchPoint = [[touches anyObject] locationInView:self];
if (CGRectContainsPoint(self.rightArrowFrame, touchPoint)) {
_touchState = TouchStateRightArrow;
}
}
This method retrieves the point where the user touched the view and then checks to see if the point is within the bounds of the right arrowhead’s frame. If so, then _touchState
is set to indicate that the user has tapped the right arrowhead and can begin dragging.
So far you’ve handled the cases where a user begins and cancels touches on your arrow. But the most complex logic comes in as a user drags the arrow around.
Add the following code to BezierView.m:
-(void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
// 1
if (_touchState == TouchStateRightArrow) {
// 2
CGPoint touchPoint = [[touches anyObject] locationInView:self];
// 3
if (touchPoint.y >= _leftArrowTip.y) {
// 4
_leftArrowTip = CGPointMake(0, kArrowFrameHeightHalf);
_rightArrowTip = CGPointMake(_rightArrowTip.x, touchPoint.y);
self.frame = CGRectMake(_initialOrigin.x, _initialOrigin.y,
self.frame.size.width, touchPoint.y + kArrowFrameHeightHalf);
} else {
// 5
CGFloat newYPosition = [self convertPoint:touchPoint
toView:self.superview].y - kArrowFrameHeightHalf;
CGFloat newHeight = _initialLeftArrowTipY - newYPosition + kArrowFrameHeightHalf;
// 6
self.frame = CGRectMake(_initialOrigin.x, newYPosition, self.frame.size.width, newHeight);
_rightArrowTip = CGPointMake(_rightArrowTip.x, kArrowFrameHeightHalf);
_leftArrowTip = CGPointMake(_leftArrowTip.x, newHeight - kArrowFrameHeightHalf);
}
// 7
[self setNeedsDisplay];
}
}
Here’s a review of the above code, comment by comment:
- Check that you are in the
TouchStateRightArrow
state, meaning the user tapped within the right arrow frame. If so, the arrow head can be moved. - Acquire the touch point and convert it to local view coordinates.
- If the touch point is higher than the left arrowhead’s Y position, then the right arrow head is located above the left arrowhead and you enter the
if
block. Otherwise, it must be below the left arrowhead’s Y position, and you enter theelse
block. - If the right arrow is above the left arrow, calculate the new positions for the left and right arrowheads. Next, set the view’s frame using
_initialOrigin
for the X and Y values, the view’s current width for the new width since the right arrowhead can only be dragged up and down, and half of the frame’s height plus the touch point’s Y value for the new height of the frame. - If the right arrowhead is lower than the left arrowhead, there’s a bit more code. Retrieve the new Y position of the view’s frame using
convertPoint:toView:
. The new height of the frame is calculated by subtracting the new Y position from the initial left arrowhead’s Y position, and then adding half of the frame height of an arrowhead. - Set the view’s frame and the new coordinates for the left and right arrowheads.
- Call
setNeedsDisplay
to indicate that the view has changed and needs to be redrawn.
There’s one small method left to add before you can finish this part off.
Add the following method to BezierView.m:
-(void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
_touchState = TouchStateInvalid;
if (self.releaseHandler) {
CGPoint superViewPosition = [self convertPoint:_rightArrowTip
toView:self.superview];
self.releaseHandler(superViewPosition);
}
}
The above method first sets _touchState
to indicate that no dragging is taking place. It then checks to see if there’s a release handler. If so, the method calls it and passes the release point of the right arrowhead after converting the position to the parent view’s coordinate system.
If you didn’t convert the coordinates, then your values would be relative to the coordinate system of the BezierView
, which would cause problems when drawing your arrow!
The time has come to test your work!
Build and run the project, switch to the Bezier tab, tap the right arrow and try dragging it up and down:
Your dynamic, draggable arrow is fully functional. However, creating a custom component is only half the fun — using your custom control creatively in an app takes it to the next level!