PaintCode Tutorial: Bezier Paths



Learn how to create dynamic curved arrows!

Welcome to our third and final part of our PaintCode tutorial series!

PaintCode is a neat app where you can draw user interfaces like in Photoshop – but instead of generating an image, it generates Core Graphics code.

In the first part of the series, you learned how you could use PaintCode to create beautiful resizable and recolorable buttons.

In the second part of the series, you learned how to create your own custom progress bar design using PaintCode, and then integrate it into your app.

In this third and final part of the series, you’ll create an interactive quiz app. One side of the app will have a list of questions, and the other side a list of answers.

The user must match up each question with the correct answer by dragging an arrow from the question to the answer. In the process of doing this, you’ll learn how to create dynamic, resizable shapes using bezier paths.

If you’ve already completed Part 1 or Part 2, you can use your project from that tutorial as a starting point for this tutorial. However, if you don’t have your original project, or want to jump right in and get started, then download the Part 3 Starter Project.

Without further ado, open up PaintCode to get started on your last — and coolest — dynamic UI control!

Getting Started

Open a new PaintCode document and name it DynamicArrow. As before, you’ll need to shrink the canvas size and change the background color to something suitable for your control.

Click the Canvas button at the bottom right of the main view and change the width and height to 320 and 200pixels, as shown in the screenshot below:

Edit canvas size in PaintCode

Change the color of the canvas by clicking the Underlay color swatch. Enter RGB values of 50 50 50, as shown below:

choose canvas color in PaintCode

To make the arrows, you’ll need to use the Bezier tool, which allows you to use anchor points and handles to control the shape of the curve.

Drawing the Bezier Arrow

Click the Bezier tool on the top toolbar, as highlighted in the screenshot below:

bezier icon in PaintCode

Click once on the canvas to place your first point for your shape. Start on the left side of the canvas, in order to leave enough room for the rest of the arrow you’re about to draw.

Hold down the Shift key to lock the line at a 45 degree angle and move your cursor down and to the left to place the next point, as shown in the image below:

drawing shapes in PaintCode

Continue your way around the arrow shape, clicking once on the canvas to place each point. If you continue to hold down Shift, PaintCode will snap the lines to 45-degree increments, helping you keep them perfectly aligned horizontally, vertically, or inclined at 45 degrees.

Blue guide lines will appear to help you align each corner with the others, as shown in the two images below:

drawing shapes in PaintCode

Close the shape by releasing Shift and clicking once more on the first point you created. Notice how your cursor changes to a little hand when you hover over the starting point, as illustrated below:

drawing shapes in PaintCode

Note: Make sure you aren’t holding down Shift when you close the shape; otherwise you’ll get an open shape since the last two points won’t connect.

Once you have a rough shape, you’ll need to move the points around a bit. If the dots representing the points aren’t showing up already, double click anywhere on the shape to make them appear so you can edit them.

To edit the coordinates of a point, either drag it to the correct location, or click on the point to select it and set theX and Y values on the left panel.

Adjust the coordinates of the bezier points in your shape to match those in the image below:

Arrow point positions

Note: If you choose to align the points by dragging them around, the resulting coordinates may occasionally be off by about 0.5. After you’ve moved all of your points around, double-check your point coordinates to make sure they match the image above.

If you haven’t been saving your work all along, now would be a good time to do just that! :]

Now you can set the color, fill, stroke, and gradients of your arrow shape.

Adding Gradients and Colors to the Bezier Arrow

Select your shape, click the drop down menu next to Fill, and choose Add New Gradient…. Select the left gradient stop and set the Stop Color drop down to Fill Color 2. Then click the color swatch next to the drop down and set the RGB values to 140 160 192.

Similarly, set the right gradient’s Stop Color drop down to Fill Color and the color value to 56 68 98, as shown in the image below:

10_fillcolor1

Next, select Stroke Color from the drop down menu next to Stroke. In the resulting color dialog, set Base Colorto Fill Color, change the operation to Apply Shadow, and drag the Amount slider over to 70%, as shown below:

stroke color in PaintCode

Note: Basing your stroke color on the fill color means that if you ever change the fill color of your shape, the stroke color will automatically update to a darker shade of the fill color. Neat!

Add a highlight to your shape by selecting Add New Shadow… from the drop down next to Inner Shadow. Set the RBG values of your shadow to 255 255 255Offset Y to -1, and Blur Radius to 2, as shown below:

highlight in PaintCode

Add a shadow by selecting Add Shadow… from the Outer Shadow drop down. Give the shadow RBGA values of 0 0 0 62Offset Y to -1, and Blur Radius to 2, as shown below:

shadow in PaintCode

The styling of your arrow is done. The only thing left to do is give the arrow some handles for the user to manipulate when they use it in your app.

Adding Handles to the Bezier Arrow

Double-click on the shape to reveal all of its points, and click on the point shown below:

edit points in PaintCode

When your user drags the arrow around the screen, you want the body of the arrow to follow a nice bezier curve, but the two heads of the arrow should not follow the bezier transform and remain “pointy”.

Right-click on the selected point (or Control-click if you have a single-button mouse), and choose Make Point Round from the popup, as shown below:

adding bezier handles in PaintCode

Here’s the result:

adding bezier handles in PaintCode

Yes, it looks odd, but don’t panic — you’re not done yet! Hold down Option to move the handles independently of each other, start dragging the left handle up, hold down Shift to snap the point vertically, and release the Option key.

The following image explains this action in a little more detail:

adding bezier handles in PaintCode

Note: If you’re having trouble, make sure you first select the handle with Option and press down on Shift only after you start dragging the handle. Let go of the Option key at this point. Starting with both Option and Shift held down doesn’t seem to work properly.

Now hold down Option to select the right handle, move it slightly, hold down Shift, release Option and drag until the line snaps to the horizontal axis. Your arrow should look normal once again, as in the following screenshot:

adding bezier handles in PaintCode

Note: When you’re done dragging the right handle, you might notice that the left handle is no longer in place. This usually happens if you drag while holding both Option and Shift. So remember to let go of Option after you start dragging, and then hold down Shift.

Perform the same operation to each of the inner corner points to finish creating the handles that will cause the arrow to curve nicely when you move the arrowheads.

To reference the location of your arrowheads using a (0,0) coordinate origin, you’ll need to move the origin of your shape in PaintCode. Click on the double-headed blue arrow on the top-left corner of your canvas and drag it down and to the right until it snaps to the top-left corner of the arrow shape, as shown below:

Repositioning origin

If you check the position values for your bezier points, you’ll notice that they have changed. They are now set relative to the new origin point.

Now you need to add a Frame to each of the arrowheads to allow your user to manipulate the arrowheads in your app.

Adding Frames to the Bezier Arrow

Click the Frame tool, and drag a frame around the left arrowhead. You’ll find that you won’t be able to start from the origin since this ends up dragging/moving the origin point. Instead, drag out a larger frame and then set its properties as follows:

  • X: 0
  • Y: 0
  • Width: 30
  • Height: 38

This locks each of the points in the left arrowhead in place relative to the frame.

Click the Frame icon again, and drag a frame around the right arrowhead. Then set its properties as follows:

  • X: 170
  • Y: 0
  • Width: 30
  • Height: 38

Save your document and then click on the right frame (not the shape!) and try dragging it downwards. The right arrowhead should move down with the frame, but the left arrowhead will stay in place, while the connecting line curves nicely to connect them, as shown in the screenshot below:

Dragging right frame

Now try to stretch the arrow horizontally; you’ll notice the arrow stretches nicely without deforming the arrowheads, as demonstrated below:

Straight dragging

If you drag the frame for an arrowhead really far down, you’ll notice that the arrow flattens out or even starts to twist. This looks pretty unpolished — but fortunately, it’s easy to fix.

Double-click the shape to bring up its points for edit, and click on the point shown in the image below:

New arrow bezier point

Drag the point’s handle a little further to the right while holding down Shift to snap the point to the horizontal axis.

Do the same with the other three points, dragging the horizontal handle further into the body of the arrow.

Here’s the resulting curvier arrow:

Final arrow

Save your work — you’re done with the PaintCode portion of this tutorial. Now you’re all set to use your dynamic bezier arrows in your app!

Adding the Bezier Arrow to Your App

Open up your project open in Xcode. In the Project Navigator, expand the Classes > Views folder, right-click theViews folder, and select New File…. Use the iOS\Cocoa Touch\Objective-C class template, name the classBezierView, and make it a subclass of UIView.

Open BezierView.m, delete the initWithFrame: implementation, and uncomment drawRect: so that the file looks like this:

#import "BezierView.h"
 
@implementation BezierView
 
- (void)drawRect:(CGRect)rect
{
    // Drawing code
}
 
@end

In the previous parts of this series you created a subclass and immediately put the drawing code insidedrawRect:. This time, the control is a bit more complex, as you need to handle the touch interactions for moving the arrow heads.

UIResponder will be used to handle the touch events; as well, you’ll need a custom initializer for the arrow which takes as arguments the coordinates of the left and right arrow tips and a completion handler. The completion handler will be called when the user stops dragging the right arrow head.

Therefore, you’ll need to set up some supporting code before you can implement the code generated by PaintCode.

Open up BezierView.h and add the following line directly below the #import line:

typedef void(^BezierViewReleaseHandler)(CGPoint releasePoint);

This is a typedef for a block. A block of this type will be called when the user stops dragging the right arrow head. The block has the final position of the right arrow tip as a parameter so that the containing view controller knows where the right arrow tip was placed.

Now, add the following code between the @interface and @end lines:

-(id)initWithLeftArrowTipPoint:(CGPoint)leftArrowTip
            rightArrowTipPoint:(CGPoint)rightArrowTip
                releaseHandler:(BezierViewReleaseHandler)releaseHandler;

This declares the custom initializer for the arrow object. The two CGPoint variables indicate the initial position of the left and right arrow heads. The release handler will be called when the user stops dragging the right arrow head.

Open up BezierView.m and add the following constant definitions to the top of the file, directly below the#import line:

#define kArrowFrameHeight       38.0
#define kArrowFrameHeightHalf   19.0
#define kArrowFrameWidth        30.0
#define kArrowFrameWidthHalf    15.0

If you look at the PaintCode document for your dynamic arrow and select one of the arrow frames, you will see that it is 30 points wide and 38 points high. These constants prevent hardcoding these values in your app’s code. If you eventually change the size of the arrowhead frames, all you have to do is change the above constants.

Next, add the following enum to BezierView.m, directly below the constants you added in the previous step:

typedef enum {
    TouchStateInvalid,
    TouchStateRightArrow
} TouchStates;

Since you only support dragging the right arrowhead, the possible states are either TouchStateRightArrow, to indicate that the user has tapped within the right arrowhead’s frame, or TouchStateInvalid, to indicate that the user has tapped elsewhere.

Now add the following class extension to BezierView.m, directly below the enum you added in the previous step:

@interface BezierView () {
    CGFloat _initialLeftArrowTipY;
    CGPoint _initialOrigin;
    CGPoint _leftArrowTip;
    CGPoint _rightArrowTip;
    TouchStates _touchState;
}
 
@property (copy, nonatomic) BezierViewReleaseHandler releaseHandler;
 
@end

The variables above store the initial Y position of the left arrow tip, the initial origin frame, the left and right arrow tip positions, and the current touch state of the view.

Block references always need to be copied when they are stored in a variable. So there code also has a property to store the release handler received via the custom initializer.

Speaking of the custom initializer, now would be a great time to implement it!

Add the code below to BezierView.m:

-(id)initWithLeftArrowTipPoint:(CGPoint)leftArrowTip
            rightArrowTipPoint:(CGPoint)rightArrowTip
                releaseHandler:(BezierViewReleaseHandler)releaseHandler {
    // 1
    CGFloat xPosition = leftArrowTip.x;
    CGFloat yPosition = leftArrowTip.y >= rightArrowTip.y ? rightArrowTip.y : leftArrowTip.y;
    yPosition = yPosition - kArrowFrameHeightHalf;
 
    // 2
    CGFloat width = fabsf(rightArrowTip.x - leftArrowTip.x);
    CGFloat height = fabsf(rightArrowTip.y - leftArrowTip.y) + kArrowFrameHeight;
 
    CGRect frame = CGRectMake(xPosition, yPosition, width, height);
 
    if (self = [super initWithFrame:frame]) {
        // 3
        CGFloat leftYPosition = leftArrowTip.y >= rightArrowTip.y ? height - kArrowFrameHeightHalf : kArrowFrameHeightHalf;
        CGFloat rightYPosition = rightArrowTip.y >= leftArrowTip.y ? height - kArrowFrameHeightHalf : kArrowFrameHeightHalf;
 
        // 4
        _initialLeftArrowTipY = leftArrowTip.y;
        _initialOrigin = self.frame.origin;
        _leftArrowTip = CGPointMake(0, leftYPosition);
        _rightArrowTip = CGPointMake(width, rightYPosition);
 
        // 5
        _touchState = TouchStateInvalid;
        self.releaseHandler = releaseHandler;
 
        self.backgroundColor = [UIColor clearColor];
    }
 
    return self;
}

Take a minute to walk through the numbered comments one by one:

  1. Store the initial X position of the leftArrowTip parameter in a local variable called xPosition. Then do the same for the Y position — except this time check to see which arrowhead is higher on the screen. Take that arrowhead’s Y position and subtract half of the arrow frame’s height. This gives you the Y position of the frame itself, instead of the arrowhead’s Y position.
  2. Calculate the width and height of the frame by getting the absolute value between each arrowhead’s initial X and Y position. Then use these X and Y values, as well as the width and height, to create and store the view frame in a variable.
  3. If the super class’ initWithFrame: call was successful, then proceed with initialization by determining the left and right arrowhead Y coordinates. This is done with a ternary operator which again depends on which arrowhead is higher on the screen.
  4. Set some initial values including the initial Y position of the left arrow tip, the initial origin of the view, the left arrow tip position and the right arrow tip position.
  5. Set the initial touch state to invalid and store the release handler. Finally, clear the view’s background color and return a reference to the created instance.

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:

Dynamic arrow PaintCode settings

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.

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:

Arrow first run

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:

Arrow second run

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.

Drag all the things

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;
 
    ...
}

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:

  1. 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.
  2. Acquire the touch point and convert it to local view coordinates.
  3. 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 the else block.
  4. 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.
  5. 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.
  6. Set the view’s frame and the new coordinates for the left and right arrowheads.
  7. 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 theBezierView, 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:

Dragging the right arrow

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!

Putting Your Bezier Arrows to Work

To put your BezierView to use, you’re going to make a simple quiz app where your users have to drag the arrow to the correct answer.

Open Mainstoryboard.storyboard and drag some Label objects from the Object Library to the Bezier View Controller to represent some simple math questions and answers. The focus here is on your dynamic bezier arrows and making them work, rather than challenging the user!

Arrange your scene to look like the following:

Storyboard labels

Set the width of all the left-hand labels the same. Then set the height for the right-hand labels to around 40 points or so. The reason for making the right labels so tall is to give the user more real estate to drop their arrow on. With a smaller label, the user would have to be a little more precise to drop the arrow on target.

Set the color of the right-hand labels to red, indicating that each answer is incorrect. You’ll change the color of each label as the user makes a correct selection.

Finally, set the tag for the right-hand labels as follows:

  • Top label: 1
  • Middle label: 2
  • Bottom label: 3

Now you need some outlets referencing the labels.

Open BezierViewController.m and add the following class extension above the @implementation line:

@interface BezierViewController () {
    NSMutableArray *_answersArray;
}
 
@property (weak, nonatomic) IBOutlet UILabel *bottomAnswerLabel;
@property (weak, nonatomic) IBOutlet UILabel *midAnswerLabel;
@property (weak, nonatomic) IBOutlet UILabel *topAnswerLabel;
 
@end

The above code adds an internal array which will hold the current answer state for each question, along with three outlets for the right-hand answer labels. With the labels identified by tags and the corresponding answer state in an array, you can use the same release handler code to handle all three answer labels.

Switch back to the storyboard and connect each right-hand label to the matching outlet, as shown in the screenshot below:

Labels outlets connecte

Back in BezierViewController.m, replace the current viewDidLoad implementation with the following code:

-(void)viewDidLoad {
    [super viewDidLoad];
 
    // 1
    _answersArray = [NSMutableArray arrayWithObjects:@0, @0, @0, nil];
 
    // 2    
    __weak BezierViewController *weakSelf = self;
 
    // 3
    BezierView *topArrow = [[BezierView alloc] initWithLeftArrowTipPoint:CGPointMake(85, 31)
                                                      rightArrowTipPoint:CGPointMake(275, 91)
                                                          releaseHandler:^(CGPoint releasePoint) {
                                [weakSelf processInteractionWithReleasePoint:releasePoint
                                                              forViewWithTag:2];
                                                          }];
    [self.view addSubview:topArrow];
 
    BezierView *midArrow = [[BezierView alloc] initWithLeftArrowTipPoint:CGPointMake(85, 206)
                                                      rightArrowTipPoint:CGPointMake(275, 206)
                                                          releaseHandler:^(CGPoint releasePoint) {
                                [weakSelf processInteractionWithReleasePoint:releasePoint
                                                              forViewWithTag:3];
                                                          }];
    [self.view addSubview:midArrow];
 
    BezierView *bottomArrow = [[BezierView alloc] initWithLeftArrowTipPoint:CGPointMake(85, 381)
                                                         rightArrowTipPoint:CGPointMake(275, 321)
                                                             releaseHandler:^(CGPoint releasePoint) {
                                [weakSelf processInteractionWithReleasePoint:releasePoint
                                                              forViewWithTag:1];
                                                             }];
    [self.view addSubview:bottomArrow];
}

Here’s a breakdown of the above code:

  1. Initialize the array with three zero values, since the user hasn’t attempted any answers yet and all the answer states are false.
  2. Create a weak pointer to self. This is to avoid retain cycles inside the release handler block.
  3. Create three BezierView instances to match the questions. Set the left arrowhead point of each arrow to the center of the left question labels, and the right arrow tip to a point between the answers. For the release handler, call processInteractionWithReleasePoint:forViewWithTag:, which determines where the arrow was released, and checks the answer to see if it is correct.

Note: The left arrowhead points specified in the above code might not match the layout you set up. So calculate the center point for each of your left-hand labels and modify the above code accordingly.

Right now you will notice a few Xcode errors because you haven’t implemented the release handler method.

Add the following code to (still in BezierView.m):

-(void)processInteractionWithReleasePoint:(CGPoint)releasePoint forViewWithTag:(NSInteger)tag {
    UILabel *answerLabel = (UILabel *)[self.view viewWithTag:tag];
 
    CGPoint updatedReleasePoint = CGPointMake(CGRectGetMidX(answerLabel.frame), releasePoint.y);
 
    if (CGRectContainsPoint(answerLabel.frame, updatedReleasePoint)) {
        answerLabel.textColor = [UIColor greenColor];
 
        _answersArray[tag - 1] = @1;
    } else {
        answerLabel.textColor = [UIColor redColor];
 
        _answersArray[tag - 1] = @0;
    }
 
    __block NSInteger correctAnswers = 0;
 
    [_answersArray enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
        correctAnswers += [obj integerValue];
    }];
 
    if (correctAnswers == 3) {
        UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:@"Yay"
                                                            message:@"You've successully answered all the questions!"
                                                           delegate:nil
                                                  cancelButtonTitle:@"OK"
                                                  otherButtonTitles:nil];
        [alertView show];
    }}

In the code above, you retrieve the label matching the passed-in tag. If the arrow was released within the label’s frame, update the answers array and set the label’s color to green to indicate a correct answer. Otherwise, update the answers array and set the label’s color to red.

Finally, add up the number of correct answers and check if all questions have been answered. If so, show an alert with a message indicating success.

Build and run the project, switch to the Bezier tab, and drag the arrows to the correct answers as shown below:

Drag test run

If you play with the app for any length of time, you may notice a small problem where you tap on an arrow, but nothing happends. This is because the topmost arrow in the Z-order always detects touches — but that may not be the arrow your user intended to move.

To fix this, add the following code to BezierView.m:

-(BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
    if (CGRectContainsPoint(self.rightArrowFrame, point)) {
        return YES;
    }
 
    return NO;
}

The above code limits touch detection to only the right arrowhead, instead of the complete view.

pointInside:withEvent: is a UIView method which, as the name implies, returns whether a given point is inside the frame of the view. You override it to check whether the touch point received by the view is inside the right arrow’s frame. If so, then return YES so that a touch on the arrow registers properly. Otherwise, return NO and pass the touch to other subviews of BezierViewController.

Build and run your app again, and again drag the arrows to the correct answers:

Correct Answers

Hey, that works a lot better. You can now drag the arrows regardless of their order in the view and view hierarchy.

Where To Go From Here?

You can download the final project with the PaintCode file and Xcode project here.

Take a moment to reflect on how far you’ve come in this tutorial series. You’ve worked on three different projects, each with its own characteristics and challenges; you’ve learned how to use a new development tool — PaintCode; and you’ve learned how to take the code generated by PaintCode and use it in your iOS projects.

If you didn’t know anything about PaintCode coming into this series, then you should feel pretty proud about what you’ve accomplished!

PaintCode is a fantastic application that will save you hours of work and let you unleash your creativity. The best part is that you don’t have to be an artist to get great-looking results!

Thanks so much for joining me in this series of tutorials, and I hope you’ve enjoyed yourself along the way! As always, if you have any suggestions, questions, or other comments, or you want to share your PaintCode creations, feel free to join the forum discussion below.


Learn how to create dynamic curved arrows!

Welcome to our third and final part of our PaintCode tutorial series!

PaintCode is a neat app where you can draw user interfaces like in Photoshop – but instead of generating an image, it generates Core Graphics code.

In the first part of the series, you learned how you could use PaintCode to create beautiful resizable and recolorable buttons.

In the second part of the series, you learned how to create your own custom progress bar design using PaintCode, and then integrate it into your app.

In this third and final part of the series, you’ll create an interactive quiz app. One side of the app will have a list of questions, and the other side a list of answers.

The user must match up each question with the correct answer by dragging an arrow from the question to the answer. In the process of doing this, you’ll learn how to create dynamic, resizable shapes using bezier paths.

If you’ve already completed Part 1 or Part 2, you can use your project from that tutorial as a starting point for this tutorial. However, if you don’t have your original project, or want to jump right in and get started, then download the Part 3 Starter Project.

Without further ado, open up PaintCode to get started on your last — and coolest — dynamic UI control!

Getting Started

Open a new PaintCode document and name it DynamicArrow. As before, you’ll need to shrink the canvas size and change the background color to something suitable for your control.

Click the Canvas button at the bottom right of the main view and change the width and height to 320 and 200pixels, as shown in the screenshot below:

Edit canvas size in PaintCode

Change the color of the canvas by clicking the Underlay color swatch. Enter RGB values of 50 50 50, as shown below:

choose canvas color in PaintCode

To make the arrows, you’ll need to use the Bezier tool, which allows you to use anchor points and handles to control the shape of the curve.

Drawing the Bezier Arrow

Click the Bezier tool on the top toolbar, as highlighted in the screenshot below:

bezier icon in PaintCode

Click once on the canvas to place your first point for your shape. Start on the left side of the canvas, in order to leave enough room for the rest of the arrow you’re about to draw.

Hold down the Shift key to lock the line at a 45 degree angle and move your cursor down and to the left to place the next point, as shown in the image below:

drawing shapes in PaintCode

Continue your way around the arrow shape, clicking once on the canvas to place each point. If you continue to hold down Shift, PaintCode will snap the lines to 45-degree increments, helping you keep them perfectly aligned horizontally, vertically, or inclined at 45 degrees.

Blue guide lines will appear to help you align each corner with the others, as shown in the two images below:

drawing shapes in PaintCode

Close the shape by releasing Shift and clicking once more on the first point you created. Notice how your cursor changes to a little hand when you hover over the starting point, as illustrated below:

drawing shapes in PaintCode

Note: Make sure you aren’t holding down Shift when you close the shape; otherwise you’ll get an open shape since the last two points won’t connect.

Once you have a rough shape, you’ll need to move the points around a bit. If the dots representing the points aren’t showing up already, double click anywhere on the shape to make them appear so you can edit them.

To edit the coordinates of a point, either drag it to the correct location, or click on the point to select it and set theX and Y values on the left panel.

Adjust the coordinates of the bezier points in your shape to match those in the image below:

Arrow point positions

Note: If you choose to align the points by dragging them around, the resulting coordinates may occasionally be off by about 0.5. After you’ve moved all of your points around, double-check your point coordinates to make sure they match the image above.

If you haven’t been saving your work all along, now would be a good time to do just that! :]

Now you can set the color, fill, stroke, and gradients of your arrow shape.

Adding Gradients and Colors to the Bezier Arrow

Select your shape, click the drop down menu next to Fill, and choose Add New Gradient…. Select the left gradient stop and set the Stop Color drop down to Fill Color 2. Then click the color swatch next to the drop down and set the RGB values to 140 160 192.

Similarly, set the right gradient’s Stop Color drop down to Fill Color and the color value to 56 68 98, as shown in the image below:

10_fillcolor1

Next, select Stroke Color from the drop down menu next to Stroke. In the resulting color dialog, set Base Colorto Fill Color, change the operation to Apply Shadow, and drag the Amount slider over to 70%, as shown below:

stroke color in PaintCode

Note: Basing your stroke color on the fill color means that if you ever change the fill color of your shape, the stroke color will automatically update to a darker shade of the fill color. Neat!

Add a highlight to your shape by selecting Add New Shadow… from the drop down next to Inner Shadow. Set the RBG values of your shadow to 255 255 255Offset Y to -1, and Blur Radius to 2, as shown below:

highlight in PaintCode

Add a shadow by selecting Add Shadow… from the Outer Shadow drop down. Give the shadow RBGA values of 0 0 0 62Offset Y to -1, and Blur Radius to 2, as shown below:

shadow in PaintCode

The styling of your arrow is done. The only thing left to do is give the arrow some handles for the user to manipulate when they use it in your app.

Adding Handles to the Bezier Arrow

Double-click on the shape to reveal all of its points, and click on the point shown below:

edit points in PaintCode

When your user drags the arrow around the screen, you want the body of the arrow to follow a nice bezier curve, but the two heads of the arrow should not follow the bezier transform and remain “pointy”.

Right-click on the selected point (or Control-click if you have a single-button mouse), and choose Make Point Round from the popup, as shown below:

adding bezier handles in PaintCode

Here’s the result:

adding bezier handles in PaintCode

Yes, it looks odd, but don’t panic — you’re not done yet! Hold down Option to move the handles independently of each other, start dragging the left handle up, hold down Shift to snap the point vertically, and release the Option key.

The following image explains this action in a little more detail:

adding bezier handles in PaintCode

Note: If you’re having trouble, make sure you first select the handle with Option and press down on Shift only after you start dragging the handle. Let go of the Option key at this point. Starting with both Option and Shift held down doesn’t seem to work properly.

Now hold down Option to select the right handle, move it slightly, hold down Shift, release Option and drag until the line snaps to the horizontal axis. Your arrow should look normal once again, as in the following screenshot:

adding bezier handles in PaintCode

Note: When you’re done dragging the right handle, you might notice that the left handle is no longer in place. This usually happens if you drag while holding both Option and Shift. So remember to let go of Option after you start dragging, and then hold down Shift.

Perform the same operation to each of the inner corner points to finish creating the handles that will cause the arrow to curve nicely when you move the arrowheads.

To reference the location of your arrowheads using a (0,0) coordinate origin, you’ll need to move the origin of your shape in PaintCode. Click on the double-headed blue arrow on the top-left corner of your canvas and drag it down and to the right until it snaps to the top-left corner of the arrow shape, as shown below:

Repositioning origin

If you check the position values for your bezier points, you’ll notice that they have changed. They are now set relative to the new origin point.

Now you need to add a Frame to each of the arrowheads to allow your user to manipulate the arrowheads in your app.

Adding Frames to the Bezier Arrow

Click the Frame tool, and drag a frame around the left arrowhead. You’ll find that you won’t be able to start from the origin since this ends up dragging/moving the origin point. Instead, drag out a larger frame and then set its properties as follows:

  • X: 0
  • Y: 0
  • Width: 30
  • Height: 38

This locks each of the points in the left arrowhead in place relative to the frame.

Click the Frame icon again, and drag a frame around the right arrowhead. Then set its properties as follows:

  • X: 170
  • Y: 0
  • Width: 30
  • Height: 38

Save your document and then click on the right frame (not the shape!) and try dragging it downwards. The right arrowhead should move down with the frame, but the left arrowhead will stay in place, while the connecting line curves nicely to connect them, as shown in the screenshot below:

Dragging right frame

Now try to stretch the arrow horizontally; you’ll notice the arrow stretches nicely without deforming the arrowheads, as demonstrated below:

Straight dragging

If you drag the frame for an arrowhead really far down, you’ll notice that the arrow flattens out or even starts to twist. This looks pretty unpolished — but fortunately, it’s easy to fix.

Double-click the shape to bring up its points for edit, and click on the point shown in the image below:

New arrow bezier point

Drag the point’s handle a little further to the right while holding down Shift to snap the point to the horizontal axis.

Do the same with the other three points, dragging the horizontal handle further into the body of the arrow.

Here’s the resulting curvier arrow:

Final arrow

Save your work — you’re done with the PaintCode portion of this tutorial. Now you’re all set to use your dynamic bezier arrows in your app!

Adding the Bezier Arrow to Your App

Open up your project open in Xcode. In the Project Navigator, expand the Classes > Views folder, right-click theViews folder, and select New File…. Use the iOS\Cocoa Touch\Objective-C class template, name the classBezierView, and make it a subclass of UIView.

Open BezierView.m, delete the initWithFrame: implementation, and uncomment drawRect: so that the file looks like this:

#import "BezierView.h"
 
@implementation BezierView
 
- (void)drawRect:(CGRect)rect
{
    // Drawing code
}
 
@end

In the previous parts of this series you created a subclass and immediately put the drawing code insidedrawRect:. This time, the control is a bit more complex, as you need to handle the touch interactions for moving the arrow heads.

UIResponder will be used to handle the touch events; as well, you’ll need a custom initializer for the arrow which takes as arguments the coordinates of the left and right arrow tips and a completion handler. The completion handler will be called when the user stops dragging the right arrow head.

Therefore, you’ll need to set up some supporting code before you can implement the code generated by PaintCode.

Open up BezierView.h and add the following line directly below the #import line:

typedef void(^BezierViewReleaseHandler)(CGPoint releasePoint);

This is a typedef for a block. A block of this type will be called when the user stops dragging the right arrow head. The block has the final position of the right arrow tip as a parameter so that the containing view controller knows where the right arrow tip was placed.

Now, add the following code between the @interface and @end lines:

-(id)initWithLeftArrowTipPoint:(CGPoint)leftArrowTip
            rightArrowTipPoint:(CGPoint)rightArrowTip
                releaseHandler:(BezierViewReleaseHandler)releaseHandler;

This declares the custom initializer for the arrow object. The two CGPoint variables indicate the initial position of the left and right arrow heads. The release handler will be called when the user stops dragging the right arrow head.

Open up BezierView.m and add the following constant definitions to the top of the file, directly below the#import line:

#define kArrowFrameHeight       38.0
#define kArrowFrameHeightHalf   19.0
#define kArrowFrameWidth        30.0
#define kArrowFrameWidthHalf    15.0

If you look at the PaintCode document for your dynamic arrow and select one of the arrow frames, you will see that it is 30 points wide and 38 points high. These constants prevent hardcoding these values in your app’s code. If you eventually change the size of the arrowhead frames, all you have to do is change the above constants.

Next, add the following enum to BezierView.m, directly below the constants you added in the previous step:

typedef enum {
    TouchStateInvalid,
    TouchStateRightArrow
} TouchStates;

Since you only support dragging the right arrowhead, the possible states are either TouchStateRightArrow, to indicate that the user has tapped within the right arrowhead’s frame, or TouchStateInvalid, to indicate that the user has tapped elsewhere.

Now add the following class extension to BezierView.m, directly below the enum you added in the previous step:

@interface BezierView () {
    CGFloat _initialLeftArrowTipY;
    CGPoint _initialOrigin;
    CGPoint _leftArrowTip;
    CGPoint _rightArrowTip;
    TouchStates _touchState;
}
 
@property (copy, nonatomic) BezierViewReleaseHandler releaseHandler;
 
@end

The variables above store the initial Y position of the left arrow tip, the initial origin frame, the left and right arrow tip positions, and the current touch state of the view.

Block references always need to be copied when they are stored in a variable. So there code also has a property to store the release handler received via the custom initializer.

Speaking of the custom initializer, now would be a great time to implement it!

Add the code below to BezierView.m:

-(id)initWithLeftArrowTipPoint:(CGPoint)leftArrowTip
            rightArrowTipPoint:(CGPoint)rightArrowTip
                releaseHandler:(BezierViewReleaseHandler)releaseHandler {
    // 1
    CGFloat xPosition = leftArrowTip.x;
    CGFloat yPosition = leftArrowTip.y >= rightArrowTip.y ? rightArrowTip.y : leftArrowTip.y;
    yPosition = yPosition - kArrowFrameHeightHalf;
 
    // 2
    CGFloat width = fabsf(rightArrowTip.x - leftArrowTip.x);
    CGFloat height = fabsf(rightArrowTip.y - leftArrowTip.y) + kArrowFrameHeight;
 
    CGRect frame = CGRectMake(xPosition, yPosition, width, height);
 
    if (self = [super initWithFrame:frame]) {
        // 3
        CGFloat leftYPosition = leftArrowTip.y >= rightArrowTip.y ? height - kArrowFrameHeightHalf : kArrowFrameHeightHalf;
        CGFloat rightYPosition = rightArrowTip.y >= leftArrowTip.y ? height - kArrowFrameHeightHalf : kArrowFrameHeightHalf;
 
        // 4
        _initialLeftArrowTipY = leftArrowTip.y;
        _initialOrigin = self.frame.origin;
        _leftArrowTip = CGPointMake(0, leftYPosition);
        _rightArrowTip = CGPointMake(width, rightYPosition);
 
        // 5
        _touchState = TouchStateInvalid;
        self.releaseHandler = releaseHandler;
 
        self.backgroundColor = [UIColor clearColor];
    }
 
    return self;
}

Take a minute to walk through the numbered comments one by one:

  1. Store the initial X position of the leftArrowTip parameter in a local variable called xPosition. Then do the same for the Y position — except this time check to see which arrowhead is higher on the screen. Take that arrowhead’s Y position and subtract half of the arrow frame’s height. This gives you the Y position of the frame itself, instead of the arrowhead’s Y position.
  2. Calculate the width and height of the frame by getting the absolute value between each arrowhead’s initial X and Y position. Then use these X and Y values, as well as the width and height, to create and store the view frame in a variable.
  3. If the super class’ initWithFrame: call was successful, then proceed with initialization by determining the left and right arrowhead Y coordinates. This is done with a ternary operator which again depends on which arrowhead is higher on the screen.
  4. Set some initial values including the initial Y position of the left arrow tip, the initial origin of the view, the left arrow tip position and the right arrow tip position.
  5. Set the initial touch state to invalid and store the release handler. Finally, clear the view’s background color and return a reference to the created instance.

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:

Dynamic arrow PaintCode settings

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.

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:

Arrow first run

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:

Arrow second run

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.

Drag all the things

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;
 
    ...
}

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:

  1. 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.
  2. Acquire the touch point and convert it to local view coordinates.
  3. 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 the else block.
  4. 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.
  5. 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.
  6. Set the view’s frame and the new coordinates for the left and right arrowheads.
  7. 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 theBezierView, 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:

Dragging the right arrow

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!

Putting Your Bezier Arrows to Work

To put your BezierView to use, you’re going to make a simple quiz app where your users have to drag the arrow to the correct answer.

Open Mainstoryboard.storyboard and drag some Label objects from the Object Library to the Bezier View Controller to represent some simple math questions and answers. The focus here is on your dynamic bezier arrows and making them work, rather than challenging the user!

Arrange your scene to look like the following:

Storyboard labels

Set the width of all the left-hand labels the same. Then set the height for the right-hand labels to around 40 points or so. The reason for making the right labels so tall is to give the user more real estate to drop their arrow on. With a smaller label, the user would have to be a little more precise to drop the arrow on target.

Set the color of the right-hand labels to red, indicating that each answer is incorrect. You’ll change the color of each label as the user makes a correct selection.

Finally, set the tag for the right-hand labels as follows:

  • Top label: 1
  • Middle label: 2
  • Bottom label: 3

Now you need some outlets referencing the labels.

Open BezierViewController.m and add the following class extension above the @implementation line:

@interface BezierViewController () {
    NSMutableArray *_answersArray;
}
 
@property (weak, nonatomic) IBOutlet UILabel *bottomAnswerLabel;
@property (weak, nonatomic) IBOutlet UILabel *midAnswerLabel;
@property (weak, nonatomic) IBOutlet UILabel *topAnswerLabel;
 
@end

The above code adds an internal array which will hold the current answer state for each question, along with three outlets for the right-hand answer labels. With the labels identified by tags and the corresponding answer state in an array, you can use the same release handler code to handle all three answer labels.

Switch back to the storyboard and connect each right-hand label to the matching outlet, as shown in the screenshot below:

Labels outlets connecte

Back in BezierViewController.m, replace the current viewDidLoad implementation with the following code:

-(void)viewDidLoad {
    [super viewDidLoad];
 
    // 1
    _answersArray = [NSMutableArray arrayWithObjects:@0, @0, @0, nil];
 
    // 2    
    __weak BezierViewController *weakSelf = self;
 
    // 3
    BezierView *topArrow = [[BezierView alloc] initWithLeftArrowTipPoint:CGPointMake(85, 31)
                                                      rightArrowTipPoint:CGPointMake(275, 91)
                                                          releaseHandler:^(CGPoint releasePoint) {
                                [weakSelf processInteractionWithReleasePoint:releasePoint
                                                              forViewWithTag:2];
                                                          }];
    [self.view addSubview:topArrow];
 
    BezierView *midArrow = [[BezierView alloc] initWithLeftArrowTipPoint:CGPointMake(85, 206)
                                                      rightArrowTipPoint:CGPointMake(275, 206)
                                                          releaseHandler:^(CGPoint releasePoint) {
                                [weakSelf processInteractionWithReleasePoint:releasePoint
                                                              forViewWithTag:3];
                                                          }];
    [self.view addSubview:midArrow];
 
    BezierView *bottomArrow = [[BezierView alloc] initWithLeftArrowTipPoint:CGPointMake(85, 381)
                                                         rightArrowTipPoint:CGPointMake(275, 321)
                                                             releaseHandler:^(CGPoint releasePoint) {
                                [weakSelf processInteractionWithReleasePoint:releasePoint
                                                              forViewWithTag:1];
                                                             }];
    [self.view addSubview:bottomArrow];
}

Here’s a breakdown of the above code:

  1. Initialize the array with three zero values, since the user hasn’t attempted any answers yet and all the answer states are false.
  2. Create a weak pointer to self. This is to avoid retain cycles inside the release handler block.
  3. Create three BezierView instances to match the questions. Set the left arrowhead point of each arrow to the center of the left question labels, and the right arrow tip to a point between the answers. For the release handler, call processInteractionWithReleasePoint:forViewWithTag:, which determines where the arrow was released, and checks the answer to see if it is correct.

Note: The left arrowhead points specified in the above code might not match the layout you set up. So calculate the center point for each of your left-hand labels and modify the above code accordingly.

Right now you will notice a few Xcode errors because you haven’t implemented the release handler method.

Add the following code to (still in BezierView.m):

-(void)processInteractionWithReleasePoint:(CGPoint)releasePoint forViewWithTag:(NSInteger)tag {
    UILabel *answerLabel = (UILabel *)[self.view viewWithTag:tag];
 
    CGPoint updatedReleasePoint = CGPointMake(CGRectGetMidX(answerLabel.frame), releasePoint.y);
 
    if (CGRectContainsPoint(answerLabel.frame, updatedReleasePoint)) {
        answerLabel.textColor = [UIColor greenColor];
 
        _answersArray[tag - 1] = @1;
    } else {
        answerLabel.textColor = [UIColor redColor];
 
        _answersArray[tag - 1] = @0;
    }
 
    __block NSInteger correctAnswers = 0;
 
    [_answersArray enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
        correctAnswers += [obj integerValue];
    }];
 
    if (correctAnswers == 3) {
        UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:@"Yay"
                                                            message:@"You've successully answered all the questions!"
                                                           delegate:nil
                                                  cancelButtonTitle:@"OK"
                                                  otherButtonTitles:nil];
        [alertView show];
    }}

In the code above, you retrieve the label matching the passed-in tag. If the arrow was released within the label’s frame, update the answers array and set the label’s color to green to indicate a correct answer. Otherwise, update the answers array and set the label’s color to red.

Finally, add up the number of correct answers and check if all questions have been answered. If so, show an alert with a message indicating success.

Build and run the project, switch to the Bezier tab, and drag the arrows to the correct answers as shown below:

Drag test run

If you play with the app for any length of time, you may notice a small problem where you tap on an arrow, but nothing happends. This is because the topmost arrow in the Z-order always detects touches — but that may not be the arrow your user intended to move.

To fix this, add the following code to BezierView.m:

-(BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
    if (CGRectContainsPoint(self.rightArrowFrame, point)) {
        return YES;
    }
 
    return NO;
}

The above code limits touch detection to only the right arrowhead, instead of the complete view.

pointInside:withEvent: is a UIView method which, as the name implies, returns whether a given point is inside the frame of the view. You override it to check whether the touch point received by the view is inside the right arrow’s frame. If so, then return YES so that a touch on the arrow registers properly. Otherwise, return NO and pass the touch to other subviews of BezierViewController.

Build and run your app again, and again drag the arrows to the correct answers:

Correct Answers

Hey, that works a lot better. You can now drag the arrows regardless of their order in the view and view hierarchy.

Where To Go From Here?

You can download the final project with the PaintCode file and Xcode project here.

Take a moment to reflect on how far you’ve come in this tutorial series. You’ve worked on three different projects, each with its own characteristics and challenges; you’ve learned how to use a new development tool — PaintCode; and you’ve learned how to take the code generated by PaintCode and use it in your iOS projects.

If you didn’t know anything about PaintCode coming into this series, then you should feel pretty proud about what you’ve accomplished!

PaintCode is a fantastic application that will save you hours of work and let you unleash your creativity. The best part is that you don’t have to be an artist to get great-looking results!

Thanks so much for joining me in this series of tutorials, and I hope you’ve enjoyed yourself along the way! As always, if you have any suggestions, questions, or other comments, or you want to share your PaintCode creations, feel free to join the forum discussion below.


https://www.raywenderlich.com/38918/paintcode-tutorial-bezier-paths

Learn how to create dynamic curved arrows!

Welcome to our third and final part of our PaintCode tutorial series!

PaintCode is a neat app where you can draw user interfaces like in Photoshop – but instead of generating an image, it generates Core Graphics code.

In the first part of the series, you learned how you could use PaintCode to create beautiful resizable and recolorable buttons.

In the second part of the series, you learned how to create your own custom progress bar design using PaintCode, and then integrate it into your app.

In this third and final part of the series, you’ll create an interactive quiz app. One side of the app will have a list of questions, and the other side a list of answers.

The user must match up each question with the correct answer by dragging an arrow from the question to the answer. In the process of doing this, you’ll learn how to create dynamic, resizable shapes using bezier paths.

If you’ve already completed Part 1 or Part 2, you can use your project from that tutorial as a starting point for this tutorial. However, if you don’t have your original project, or want to jump right in and get started, then download the Part 3 Starter Project.

Without further ado, open up PaintCode to get started on your last — and coolest — dynamic UI control!

Getting Started

Open a new PaintCode document and name it DynamicArrow. As before, you’ll need to shrink the canvas size and change the background color to something suitable for your control.

Click the Canvas button at the bottom right of the main view and change the width and height to 320 and 200pixels, as shown in the screenshot below:

Edit canvas size in PaintCode

Change the color of the canvas by clicking the Underlay color swatch. Enter RGB values of 50 50 50, as shown below:

choose canvas color in PaintCode

To make the arrows, you’ll need to use the Bezier tool, which allows you to use anchor points and handles to control the shape of the curve.

Drawing the Bezier Arrow

Click the Bezier tool on the top toolbar, as highlighted in the screenshot below:

bezier icon in PaintCode

Click once on the canvas to place your first point for your shape. Start on the left side of the canvas, in order to leave enough room for the rest of the arrow you’re about to draw.

Hold down the Shift key to lock the line at a 45 degree angle and move your cursor down and to the left to place the next point, as shown in the image below:

drawing shapes in PaintCode

Continue your way around the arrow shape, clicking once on the canvas to place each point. If you continue to hold down Shift, PaintCode will snap the lines to 45-degree increments, helping you keep them perfectly aligned horizontally, vertically, or inclined at 45 degrees.

Blue guide lines will appear to help you align each corner with the others, as shown in the two images below:

drawing shapes in PaintCode

Close the shape by releasing Shift and clicking once more on the first point you created. Notice how your cursor changes to a little hand when you hover over the starting point, as illustrated below:

drawing shapes in PaintCode

Note: Make sure you aren’t holding down Shift when you close the shape; otherwise you’ll get an open shape since the last two points won’t connect.

Once you have a rough shape, you’ll need to move the points around a bit. If the dots representing the points aren’t showing up already, double click anywhere on the shape to make them appear so you can edit them.

To edit the coordinates of a point, either drag it to the correct location, or click on the point to select it and set theX and Y values on the left panel.

Adjust the coordinates of the bezier points in your shape to match those in the image below:

Arrow point positions

Note: If you choose to align the points by dragging them around, the resulting coordinates may occasionally be off by about 0.5. After you’ve moved all of your points around, double-check your point coordinates to make sure they match the image above.

If you haven’t been saving your work all along, now would be a good time to do just that! :]

Now you can set the color, fill, stroke, and gradients of your arrow shape.

Adding Gradients and Colors to the Bezier Arrow

Select your shape, click the drop down menu next to Fill, and choose Add New Gradient…. Select the left gradient stop and set the Stop Color drop down to Fill Color 2. Then click the color swatch next to the drop down and set the RGB values to 140 160 192.

Similarly, set the right gradient’s Stop Color drop down to Fill Color and the color value to 56 68 98, as shown in the image below:

10_fillcolor1

Next, select Stroke Color from the drop down menu next to Stroke. In the resulting color dialog, set Base Colorto Fill Color, change the operation to Apply Shadow, and drag the Amount slider over to 70%, as shown below:

stroke color in PaintCode

Note: Basing your stroke color on the fill color means that if you ever change the fill color of your shape, the stroke color will automatically update to a darker shade of the fill color. Neat!

Add a highlight to your shape by selecting Add New Shadow… from the drop down next to Inner Shadow. Set the RBG values of your shadow to 255 255 255Offset Y to -1, and Blur Radius to 2, as shown below:

highlight in PaintCode

Add a shadow by selecting Add Shadow… from the Outer Shadow drop down. Give the shadow RBGA values of 0 0 0 62Offset Y to -1, and Blur Radius to 2, as shown below:

shadow in PaintCode

The styling of your arrow is done. The only thing left to do is give the arrow some handles for the user to manipulate when they use it in your app.

Adding Handles to the Bezier Arrow

Double-click on the shape to reveal all of its points, and click on the point shown below:

edit points in PaintCode

When your user drags the arrow around the screen, you want the body of the arrow to follow a nice bezier curve, but the two heads of the arrow should not follow the bezier transform and remain “pointy”.

Right-click on the selected point (or Control-click if you have a single-button mouse), and choose Make Point Round from the popup, as shown below:

adding bezier handles in PaintCode

Here’s the result:

adding bezier handles in PaintCode

Yes, it looks odd, but don’t panic — you’re not done yet! Hold down Option to move the handles independently of each other, start dragging the left handle up, hold down Shift to snap the point vertically, and release the Option key.

The following image explains this action in a little more detail:

adding bezier handles in PaintCode

Note: If you’re having trouble, make sure you first select the handle with Option and press down on Shift only after you start dragging the handle. Let go of the Option key at this point. Starting with both Option and Shift held down doesn’t seem to work properly.

Now hold down Option to select the right handle, move it slightly, hold down Shift, release Option and drag until the line snaps to the horizontal axis. Your arrow should look normal once again, as in the following screenshot:

adding bezier handles in PaintCode

Note: When you’re done dragging the right handle, you might notice that the left handle is no longer in place. This usually happens if you drag while holding both Option and Shift. So remember to let go of Option after you start dragging, and then hold down Shift.

Perform the same operation to each of the inner corner points to finish creating the handles that will cause the arrow to curve nicely when you move the arrowheads.

To reference the location of your arrowheads using a (0,0) coordinate origin, you’ll need to move the origin of your shape in PaintCode. Click on the double-headed blue arrow on the top-left corner of your canvas and drag it down and to the right until it snaps to the top-left corner of the arrow shape, as shown below:

Repositioning origin

If you check the position values for your bezier points, you’ll notice that they have changed. They are now set relative to the new origin point.

Now you need to add a Frame to each of the arrowheads to allow your user to manipulate the arrowheads in your app.

Adding Frames to the Bezier Arrow

Click the Frame tool, and drag a frame around the left arrowhead. You’ll find that you won’t be able to start from the origin since this ends up dragging/moving the origin point. Instead, drag out a larger frame and then set its properties as follows:

  • X: 0
  • Y: 0
  • Width: 30
  • Height: 38

This locks each of the points in the left arrowhead in place relative to the frame.

Click the Frame icon again, and drag a frame around the right arrowhead. Then set its properties as follows:

  • X: 170
  • Y: 0
  • Width: 30
  • Height: 38

Save your document and then click on the right frame (not the shape!) and try dragging it downwards. The right arrowhead should move down with the frame, but the left arrowhead will stay in place, while the connecting line curves nicely to connect them, as shown in the screenshot below:

Dragging right frame

Now try to stretch the arrow horizontally; you’ll notice the arrow stretches nicely without deforming the arrowheads, as demonstrated below:

Straight dragging

If you drag the frame for an arrowhead really far down, you’ll notice that the arrow flattens out or even starts to twist. This looks pretty unpolished — but fortunately, it’s easy to fix.

Double-click the shape to bring up its points for edit, and click on the point shown in the image below:

New arrow bezier point

Drag the point’s handle a little further to the right while holding down Shift to snap the point to the horizontal axis.

Do the same with the other three points, dragging the horizontal handle further into the body of the arrow.

Here’s the resulting curvier arrow:

Final arrow

Save your work — you’re done with the PaintCode portion of this tutorial. Now you’re all set to use your dynamic bezier arrows in your app!

Adding the Bezier Arrow to Your App

Open up your project open in Xcode. In the Project Navigator, expand the Classes > Views folder, right-click theViews folder, and select New File…. Use the iOS\Cocoa Touch\Objective-C class template, name the classBezierView, and make it a subclass of UIView.

Open BezierView.m, delete the initWithFrame: implementation, and uncomment drawRect: so that the file looks like this:

#import "BezierView.h"
 
@implementation BezierView
 
- (void)drawRect:(CGRect)rect
{
    // Drawing code
}
 
@end

In the previous parts of this series you created a subclass and immediately put the drawing code insidedrawRect:. This time, the control is a bit more complex, as you need to handle the touch interactions for moving the arrow heads.

UIResponder will be used to handle the touch events; as well, you’ll need a custom initializer for the arrow which takes as arguments the coordinates of the left and right arrow tips and a completion handler. The completion handler will be called when the user stops dragging the right arrow head.

Therefore, you’ll need to set up some supporting code before you can implement the code generated by PaintCode.

Open up BezierView.h and add the following line directly below the #import line:

typedef void(^BezierViewReleaseHandler)(CGPoint releasePoint);

This is a typedef for a block. A block of this type will be called when the user stops dragging the right arrow head. The block has the final position of the right arrow tip as a parameter so that the containing view controller knows where the right arrow tip was placed.

Now, add the following code between the @interface and @end lines:

-(id)initWithLeftArrowTipPoint:(CGPoint)leftArrowTip
            rightArrowTipPoint:(CGPoint)rightArrowTip
                releaseHandler:(BezierViewReleaseHandler)releaseHandler;

This declares the custom initializer for the arrow object. The two CGPoint variables indicate the initial position of the left and right arrow heads. The release handler will be called when the user stops dragging the right arrow head.

Open up BezierView.m and add the following constant definitions to the top of the file, directly below the#import line:

#define kArrowFrameHeight       38.0
#define kArrowFrameHeightHalf   19.0
#define kArrowFrameWidth        30.0
#define kArrowFrameWidthHalf    15.0

If you look at the PaintCode document for your dynamic arrow and select one of the arrow frames, you will see that it is 30 points wide and 38 points high. These constants prevent hardcoding these values in your app’s code. If you eventually change the size of the arrowhead frames, all you have to do is change the above constants.

Next, add the following enum to BezierView.m, directly below the constants you added in the previous step:

typedef enum {
    TouchStateInvalid,
    TouchStateRightArrow
} TouchStates;

Since you only support dragging the right arrowhead, the possible states are either TouchStateRightArrow, to indicate that the user has tapped within the right arrowhead’s frame, or TouchStateInvalid, to indicate that the user has tapped elsewhere.

Now add the following class extension to BezierView.m, directly below the enum you added in the previous step:

@interface BezierView () {
    CGFloat _initialLeftArrowTipY;
    CGPoint _initialOrigin;
    CGPoint _leftArrowTip;
    CGPoint _rightArrowTip;
    TouchStates _touchState;
}
 
@property (copy, nonatomic) BezierViewReleaseHandler releaseHandler;
 
@end

The variables above store the initial Y position of the left arrow tip, the initial origin frame, the left and right arrow tip positions, and the current touch state of the view.

Block references always need to be copied when they are stored in a variable. So there code also has a property to store the release handler received via the custom initializer.

Speaking of the custom initializer, now would be a great time to implement it!

Add the code below to BezierView.m:

-(id)initWithLeftArrowTipPoint:(CGPoint)leftArrowTip
            rightArrowTipPoint:(CGPoint)rightArrowTip
                releaseHandler:(BezierViewReleaseHandler)releaseHandler {
    // 1
    CGFloat xPosition = leftArrowTip.x;
    CGFloat yPosition = leftArrowTip.y >= rightArrowTip.y ? rightArrowTip.y : leftArrowTip.y;
    yPosition = yPosition - kArrowFrameHeightHalf;
 
    // 2
    CGFloat width = fabsf(rightArrowTip.x - leftArrowTip.x);
    CGFloat height = fabsf(rightArrowTip.y - leftArrowTip.y) + kArrowFrameHeight;
 
    CGRect frame = CGRectMake(xPosition, yPosition, width, height);
 
    if (self = [super initWithFrame:frame]) {
        // 3
        CGFloat leftYPosition = leftArrowTip.y >= rightArrowTip.y ? height - kArrowFrameHeightHalf : kArrowFrameHeightHalf;
        CGFloat rightYPosition = rightArrowTip.y >= leftArrowTip.y ? height - kArrowFrameHeightHalf : kArrowFrameHeightHalf;
 
        // 4
        _initialLeftArrowTipY = leftArrowTip.y;
        _initialOrigin = self.frame.origin;
        _leftArrowTip = CGPointMake(0, leftYPosition);
        _rightArrowTip = CGPointMake(width, rightYPosition);
 
        // 5
        _touchState = TouchStateInvalid;
        self.releaseHandler = releaseHandler;
 
        self.backgroundColor = [UIColor clearColor];
    }
 
    return self;
}

Take a minute to walk through the numbered comments one by one:

  1. Store the initial X position of the leftArrowTip parameter in a local variable called xPosition. Then do the same for the Y position — except this time check to see which arrowhead is higher on the screen. Take that arrowhead’s Y position and subtract half of the arrow frame’s height. This gives you the Y position of the frame itself, instead of the arrowhead’s Y position.
  2. Calculate the width and height of the frame by getting the absolute value between each arrowhead’s initial X and Y position. Then use these X and Y values, as well as the width and height, to create and store the view frame in a variable.
  3. If the super class’ initWithFrame: call was successful, then proceed with initialization by determining the left and right arrowhead Y coordinates. This is done with a ternary operator which again depends on which arrowhead is higher on the screen.
  4. Set some initial values including the initial Y position of the left arrow tip, the initial origin of the view, the left arrow tip position and the right arrow tip position.
  5. Set the initial touch state to invalid and store the release handler. Finally, clear the view’s background color and return a reference to the created instance.

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:

Dynamic arrow PaintCode settings

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.

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:

Arrow first run

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:

Arrow second run

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.

Drag all the things

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;
 
    ...
}

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:

  1. 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.
  2. Acquire the touch point and convert it to local view coordinates.
  3. 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 the else block.
  4. 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.
  5. 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.
  6. Set the view’s frame and the new coordinates for the left and right arrowheads.
  7. 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 theBezierView, 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:

Dragging the right arrow

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!

Putting Your Bezier Arrows to Work

To put your BezierView to use, you’re going to make a simple quiz app where your users have to drag the arrow to the correct answer.

Open Mainstoryboard.storyboard and drag some Label objects from the Object Library to the Bezier View Controller to represent some simple math questions and answers. The focus here is on your dynamic bezier arrows and making them work, rather than challenging the user!

Arrange your scene to look like the following:

Storyboard labels

Set the width of all the left-hand labels the same. Then set the height for the right-hand labels to around 40 points or so. The reason for making the right labels so tall is to give the user more real estate to drop their arrow on. With a smaller label, the user would have to be a little more precise to drop the arrow on target.

Set the color of the right-hand labels to red, indicating that each answer is incorrect. You’ll change the color of each label as the user makes a correct selection.

Finally, set the tag for the right-hand labels as follows:

  • Top label: 1
  • Middle label: 2
  • Bottom label: 3

Now you need some outlets referencing the labels.

Open BezierViewController.m and add the following class extension above the @implementation line:

@interface BezierViewController () {
    NSMutableArray *_answersArray;
}
 
@property (weak, nonatomic) IBOutlet UILabel *bottomAnswerLabel;
@property (weak, nonatomic) IBOutlet UILabel *midAnswerLabel;
@property (weak, nonatomic) IBOutlet UILabel *topAnswerLabel;
 
@end

The above code adds an internal array which will hold the current answer state for each question, along with three outlets for the right-hand answer labels. With the labels identified by tags and the corresponding answer state in an array, you can use the same release handler code to handle all three answer labels.

Switch back to the storyboard and connect each right-hand label to the matching outlet, as shown in the screenshot below:

Labels outlets connecte

Back in BezierViewController.m, replace the current viewDidLoad implementation with the following code:

-(void)viewDidLoad {
    [super viewDidLoad];
 
    // 1
    _answersArray = [NSMutableArray arrayWithObjects:@0, @0, @0, nil];
 
    // 2    
    __weak BezierViewController *weakSelf = self;
 
    // 3
    BezierView *topArrow = [[BezierView alloc] initWithLeftArrowTipPoint:CGPointMake(85, 31)
                                                      rightArrowTipPoint:CGPointMake(275, 91)
                                                          releaseHandler:^(CGPoint releasePoint) {
                                [weakSelf processInteractionWithReleasePoint:releasePoint
                                                              forViewWithTag:2];
                                                          }];
    [self.view addSubview:topArrow];
 
    BezierView *midArrow = [[BezierView alloc] initWithLeftArrowTipPoint:CGPointMake(85, 206)
                                                      rightArrowTipPoint:CGPointMake(275, 206)
                                                          releaseHandler:^(CGPoint releasePoint) {
                                [weakSelf processInteractionWithReleasePoint:releasePoint
                                                              forViewWithTag:3];
                                                          }];
    [self.view addSubview:midArrow];
 
    BezierView *bottomArrow = [[BezierView alloc] initWithLeftArrowTipPoint:CGPointMake(85, 381)
                                                         rightArrowTipPoint:CGPointMake(275, 321)
                                                             releaseHandler:^(CGPoint releasePoint) {
                                [weakSelf processInteractionWithReleasePoint:releasePoint
                                                              forViewWithTag:1];
                                                             }];
    [self.view addSubview:bottomArrow];
}

Here’s a breakdown of the above code:

  1. Initialize the array with three zero values, since the user hasn’t attempted any answers yet and all the answer states are false.
  2. Create a weak pointer to self. This is to avoid retain cycles inside the release handler block.
  3. Create three BezierView instances to match the questions. Set the left arrowhead point of each arrow to the center of the left question labels, and the right arrow tip to a point between the answers. For the release handler, call processInteractionWithReleasePoint:forViewWithTag:, which determines where the arrow was released, and checks the answer to see if it is correct.

Note: The left arrowhead points specified in the above code might not match the layout you set up. So calculate the center point for each of your left-hand labels and modify the above code accordingly.

Right now you will notice a few Xcode errors because you haven’t implemented the release handler method.

Add the following code to (still in BezierView.m):

-(void)processInteractionWithReleasePoint:(CGPoint)releasePoint forViewWithTag:(NSInteger)tag {
    UILabel *answerLabel = (UILabel *)[self.view viewWithTag:tag];
 
    CGPoint updatedReleasePoint = CGPointMake(CGRectGetMidX(answerLabel.frame), releasePoint.y);
 
    if (CGRectContainsPoint(answerLabel.frame, updatedReleasePoint)) {
        answerLabel.textColor = [UIColor greenColor];
 
        _answersArray[tag - 1] = @1;
    } else {
        answerLabel.textColor = [UIColor redColor];
 
        _answersArray[tag - 1] = @0;
    }
 
    __block NSInteger correctAnswers = 0;
 
    [_answersArray enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
        correctAnswers += [obj integerValue];
    }];
 
    if (correctAnswers == 3) {
        UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:@"Yay"
                                                            message:@"You've successully answered all the questions!"
                                                           delegate:nil
                                                  cancelButtonTitle:@"OK"
                                                  otherButtonTitles:nil];
        [alertView show];
    }}

In the code above, you retrieve the label matching the passed-in tag. If the arrow was released within the label’s frame, update the answers array and set the label’s color to green to indicate a correct answer. Otherwise, update the answers array and set the label’s color to red.

Finally, add up the number of correct answers and check if all questions have been answered. If so, show an alert with a message indicating success.

Build and run the project, switch to the Bezier tab, and drag the arrows to the correct answers as shown below:

Drag test run

If you play with the app for any length of time, you may notice a small problem where you tap on an arrow, but nothing happends. This is because the topmost arrow in the Z-order always detects touches — but that may not be the arrow your user intended to move.

To fix this, add the following code to BezierView.m:

-(BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
    if (CGRectContainsPoint(self.rightArrowFrame, point)) {
        return YES;
    }
 
    return NO;
}

The above code limits touch detection to only the right arrowhead, instead of the complete view.

pointInside:withEvent: is a UIView method which, as the name implies, returns whether a given point is inside the frame of the view. You override it to check whether the touch point received by the view is inside the right arrow’s frame. If so, then return YES so that a touch on the arrow registers properly. Otherwise, return NO and pass the touch to other subviews of BezierViewController.

Build and run your app again, and again drag the arrows to the correct answers:

Correct Answers

Hey, that works a lot better. You can now drag the arrows regardless of their order in the view and view hierarchy.

Where To Go From Here?

You can download the final project with the PaintCode file and Xcode project here.

Take a moment to reflect on how far you’ve come in this tutorial series. You’ve worked on three different projects, each with its own characteristics and challenges; you’ve learned how to use a new development tool — PaintCode; and you’ve learned how to take the code generated by PaintCode and use it in your iOS projects.

If you didn’t know anything about PaintCode coming into this series, then you should feel pretty proud about what you’ve accomplished!

PaintCode is a fantastic application that will save you hours of work and let you unleash your creativity. The best part is that you don’t have to be an artist to get great-looking results!

Thanks so much for joining me in this series of tutorials, and I hope you’ve enjoyed yourself along the way! As always, if you have any suggestions, questions, or other comments, or you want to share your PaintCode creations, feel free to join the forum discussion below.

發佈了54 篇原創文章 · 獲贊 5 · 訪問量 17萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章