Skip to content

Instantly share code, notes, and snippets.

@mayoff
Last active May 13, 2022 07:30
Show Gist options
  • Star 39 You must be signed in to star a gist
  • Fork 5 You must be signed in to fork a gist
  • Save mayoff/4146780 to your computer and use it in GitHub Desktop.
Save mayoff/4146780 to your computer and use it in GitHub Desktop.
UIBezierPath category to create an arrow (now with a Swift version!)
// Swift 2.2 syntax / API
import UIKit
extension UIBezierPath {
class func arrow(from start: CGPoint, to end: CGPoint, tailWidth: CGFloat, headWidth: CGFloat, headLength: CGFloat) -> Self {
let length = hypot(end.x - start.x, end.y - start.y)
let tailLength = length - headLength
func p(_ x: CGFloat, _ y: CGFloat) -> CGPoint { return CGPoint(x: x, y: y) }
var points: [CGPoint] = [
p(0, tailWidth / 2),
p(tailLength, tailWidth / 2),
p(tailLength, headWidth / 2),
p(length, 0),
p(tailLength, -headWidth / 2),
p(tailLength, -tailWidth / 2),
p(0, -tailWidth / 2)
]
let cosine = (end.x - start.x) / length
let sine = (end.y - start.y) / length
var transform = CGAffineTransform(a: cosine, b: sine, c: -sine, d: cosine, tx: start.x, ty: start.y)
let path = CGPathCreateMutable()
CGPathAddLines(path, &transform, &points, points.count)
CGPathCloseSubpath(path)
return self.init(CGPath: path)
}
}
// Example call:
let arrow = UIBezierPath.arrow(from: CGPointMake(50, 100), to: CGPointMake(200, 50),
tailWidth: 10, headWidth: 25, headLength: 40)
#import <UIKit/UIKit.h>
@interface UIBezierPath (dqd_arrowhead)
+ (UIBezierPath *)dqd_bezierPathWithArrowFromPoint:(CGPoint)startPoint
toPoint:(CGPoint)endPoint
tailWidth:(CGFloat)tailWidth
headWidth:(CGFloat)headWidth
headLength:(CGFloat)headLength;
@end
// To the extent possible under law, I (Rob Mayoff, the author of this work) have waived
// all copyright and related or neighboring rights to this work, in accordance with
// the CC0 1.0 Universal Public Domain Dedication. Please see this page
// for details: http://creativecommons.org/publicdomain/zero/1.0/
#import "UIBezierPath+dqd_arrowhead.h"
#define kArrowPointCount 7
@implementation UIBezierPath (dqd_arrowhead)
+ (UIBezierPath *)dqd_bezierPathWithArrowFromPoint:(CGPoint)startPoint
toPoint:(CGPoint)endPoint
tailWidth:(CGFloat)tailWidth
headWidth:(CGFloat)headWidth
headLength:(CGFloat)headLength {
CGFloat length = hypotf(endPoint.x - startPoint.x, endPoint.y - startPoint.y);
CGPoint points[kArrowPointCount];
[self dqd_getAxisAlignedArrowPoints:points
forLength:length
tailWidth:tailWidth
headWidth:headWidth
headLength:headLength];
CGAffineTransform transform = [self dqd_transformForStartPoint:startPoint
endPoint:endPoint
length:length];
CGMutablePathRef cgPath = CGPathCreateMutable();
CGPathAddLines(cgPath, &transform, points, sizeof points / sizeof *points);
CGPathCloseSubpath(cgPath);
UIBezierPath *uiPath = [UIBezierPath bezierPathWithCGPath:cgPath];
CGPathRelease(cgPath);
return uiPath;
}
+ (void)dqd_getAxisAlignedArrowPoints:(CGPoint[kArrowPointCount])points
forLength:(CGFloat)length
tailWidth:(CGFloat)tailWidth
headWidth:(CGFloat)headWidth
headLength:(CGFloat)headLength {
CGFloat tailLength = length - headLength;
points[0] = CGPointMake(0, tailWidth / 2);
points[1] = CGPointMake(tailLength, tailWidth / 2);
points[2] = CGPointMake(tailLength, headWidth / 2);
points[3] = CGPointMake(length, 0);
points[4] = CGPointMake(tailLength, -headWidth / 2);
points[5] = CGPointMake(tailLength, -tailWidth / 2);
points[6] = CGPointMake(0, -tailWidth / 2);
}
+ (CGAffineTransform)dqd_transformForStartPoint:(CGPoint)startPoint
endPoint:(CGPoint)endPoint
length:(CGFloat)length {
CGFloat cosine = (endPoint.x - startPoint.x) / length;
CGFloat sine = (endPoint.y - startPoint.y) / length;
return (CGAffineTransform){ cosine, sine, -sine, cosine, startPoint.x, startPoint.y };
}
@end
@mayoff
Copy link
Author

mayoff commented Nov 26, 2012

@Prakashan
Copy link

How to use it in a View?

@pragmatrix
Copy link

Nice work! For anyone interested, here is a MonoTouch / C# port: https://gist.github.com/pragmatrix/6406094

@mwermuth
Copy link

I made a Swift extension for this :) https://gist.github.com/mwermuth/07825df27ea28f5fc89a

@misbell
Copy link

misbell commented Nov 7, 2014

bless the beasts and the children, and the developers who convert code to Xamarin and Swift.

However, I rewrote the Monotouch example to use CGPoint rather than PointF, because it didn't compile, failing on the AddLines call, not liking the PointF[].

Note: I am compiling using the new Unified 64 bit compiler for OSX, not MonoTouch, so perhaps there is a difference in what will and will not work.

static class ArrowPath
{
    public static CGPath pathWithArrowFromPoint (
        PointF startPoint,
        PointF endPoint,
        float tailWidth,
        float headWidth,
        float headLength)
    {
        var dx = endPoint.X - startPoint.X;
        var dy = endPoint.Y - startPoint.Y;
        var length = (float)Math.Sqrt (dx * dx + dy * dy);
        var points = getAxisAlignedArrowPoints (length, tailWidth, headWidth, headLength);
        var transform = transformForStartPoint (startPoint, endPoint, length);

        var path = new CGPath ();
        path.AddLines (transform, points);
        path.CloseSubpath ();

        return path;
    }

    static CGPoint[] getAxisAlignedArrowPoints (
        float length,
        float tailWidth, 
        float headWidth, 
        float headLength)
    {
        var tailLength = length - headLength;
        var points = new CGPoint[7];

        points [0] = new CGPoint (0, tailWidth / 2);
        points [1] = new CGPoint (tailLength, tailWidth / 2);
        points [2] = new CGPoint (tailLength, headWidth / 2);
        points [3] = new CGPoint (length, 0);
        points [4] = new CGPoint (tailLength, -headWidth / 2);
        points [5] = new CGPoint (tailLength, -tailWidth / 2);
        points [6] = new CGPoint (0, -tailWidth / 2);

        return points;
    }

    static CGAffineTransform transformForStartPoint (CGPoint startPoint, CGPoint endPoint, float length)
    {
        var cosine = (endPoint.X - startPoint.X) / length;
        var sine = (endPoint.Y - startPoint.Y) / length;
        return new CGAffineTransform (cosine, sine, -sine, cosine, startPoint.X, startPoint.Y);
    }
}

@rajatmohanty
Copy link

@Prakashan

You need to import "UIBezierPath+dqd_arrowhead.h" file

Then you can use it like this Ex

  • (void)viewDidLoad {

    [super viewDidLoad];
    [self drawArrow];

}
-(void)drawArrow{

UIBezierPath *path=[UIBezierPath dqd_bezierPathWithArrowFromPoint:CGPointMake(50, 50)
                                                          toPoint:CGPointMake(70, 80)
                                                        tailWidth:20.0f
                                                        headWidth:10.0f
                                                       headLength:10.0f];

CAShapeLayer *shape = [CAShapeLayer layer];
shape.path = path.CGPath;
shape.fillColor = [UIColor colorWithRed:255/255.0 green:20/255.0 blue:147/255.0 alpha:1].CGColor;

[self.view.layer addSublayer:shape];

}

@csann
Copy link

csann commented Sep 5, 2016

Anyone know how to convert Arrow.swift to Swift 3 syntax?

I'm having trouble with CGPathAddLines(path, &transform, &points, points.count)

@PollyP
Copy link

PollyP commented Nov 6, 2016

Super awesome! Thanks for this gist, @mayoff!

Here is my Swift 3.0 version:

// Swift 3.0 syntax/api
import UIKit

extension UIBezierPath {
    
    class func arrow(from start: CGPoint, to end: CGPoint, tailWidth: CGFloat, headWidth: CGFloat, headLength: CGFloat) -> Self {
        let length = hypot(end.x - start.x, end.y - start.y)
        let tailLength = length - headLength
        
        func p(_ x: CGFloat, _ y: CGFloat) -> CGPoint { return CGPoint(x: x, y: y) }
        let points: [CGPoint] = [
            p(0, tailWidth / 2),
            p(tailLength, tailWidth / 2),
            p(tailLength, headWidth / 2),
            p(length, 0),
            p(tailLength, -headWidth / 2),
            p(tailLength, -tailWidth / 2),
            p(0, -tailWidth / 2)
        ]
        
        let cosine = (end.x - start.x) / length
        let sine = (end.y - start.y) / length
        let transform = CGAffineTransform(a: cosine, b: sine, c: -sine, d: cosine, tx: start.x, ty: start.y)
        
        let path = CGMutablePath()
        path.addLines(between: points, transform: transform )
        path.closeSubpath()
        return self.init(cgPath: path)
    }
    
}

Integrating it into a UIView:

override func draw(_ rect: CGRect) {
        let arrow = UIBezierPath.arrow(from: CGPoint(x:10, y:10), to: CGPoint(x:200, y:10),tailWidth: 10, headWidth: 25, headLength: 40)
        let shapeLayer = CAShapeLayer()
        shapeLayer.path = arrow.cgPath
        self.layer.addSublayer(shapeLayer)
    }

@fareast555
Copy link

Just what I needed. Incorporating it into an app now to use as a alternative to a graphic underlay. Thanks a million for sharing this. You rock!!

@rajaramanZencode
Copy link

How to draw without closed head, need only line for tail

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment