Search code examples
objective-cuibezierpath

UIBezierPath- Both direction Arrow with Curve


I am trying to make an arrow which have two side arrow mark and in middle arc curve

But Now i have used mayOff_gist and able to make single line arrow with one side .

code-

//.h

@interface UIBezierPath (arrow)


+ (UIBezierPath *)dqd_bezierPathWithArrowFromPoint:(CGPoint)startPoint
                                       toPoint:(CGPoint)endPoint
                                     tailWidth:(CGFloat)tailWidth
                                     headWidth:(CGFloat)headWidth
                                    headLength:(CGFloat)headLength;


@end

//.m

#define kArrowPointCount 7

@implementation UIBezierPath (arrow)


+ (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 };
}

and above code gives output  mark


Solution

  • The basic idea in drawing arrowheads on a curved path is to figure out the tangent of the end of the curve (e.g. the slope between the point and its control point), and then figure out how to offset the arrow head. For example, consider this extension to UIBezierPath:

    @interface UIBezierPath (ArrowHead)
    
    /**
     Function to add an arrow from the currentPoint.
    
     @param point         The point of the arrow
     @param controlPoint  The point from which the arrow is aligned. Typically, this will be the
                          control point for the previous quad/cubic bezier, or if dealing with a
                          line, the starting point.
     @param width         The width of the arrow (distance from the line).
     @param height        The height of the arrow (distance from the start point).
     @param isOpen        Whether the arrowhead is open or closed.
     */
    
    - (void)addArrowFrom:(CGPoint)point
            controlPoint:(CGPoint)controlPoint
                   width:(CGFloat)width
                  height:(CGFloat)height
                  isOpen:(BOOL)isOpen;
    
    @end
    

    And

    @implementation UIBezierPath (ArrowHead)
    
    - (void)addArrowFrom:(CGPoint)point
            controlPoint:(CGPoint)controlPoint
                   width:(CGFloat)width
                  height:(CGFloat)height
                  isOpen:(BOOL)isOpen {
        CGFloat angle = atan2f(point.y - controlPoint.y, point.x - controlPoint.x);
        CGFloat angleAdjustment = atan2f(width, -height);
        CGFloat distance = hypotf(width, height);
    
        [self moveToPoint:point];
        [self addLineToPoint:[self calculatePointFromPoint:point angle:angle + angleAdjustment distance:distance]]; // to the right
        if (isOpen) [self addLineToPoint:point];                                                                    // move back to the point
        [self addLineToPoint:[self calculatePointFromPoint:point angle:angle - angleAdjustment distance:distance]]; // to the left
        if (isOpen) {
            [self addLineToPoint:point]; // straight ahead
        } else {
            [self closePath];
        }
    }
    
    /**
     Private function for calculating a point at a particular angle from some point.
    
     @param point       The starting point.
     @param angle       The angle from that point.
     @param distance    The distance from that point.
     @return            The resulting CGPoint.
     */
    
    - (CGPoint)calculatePointFromPoint:(CGPoint)point angle:(CGFloat)angle distance:(CGFloat)distance {
        return CGPointMake(point.x + cosf(angle) * distance, point.y + sinf(angle) * distance);
    }
    
    @end
    

    You can then render a path like so:

    @interface ViewController ()
    @property (nonatomic, strong) NSMutableArray<CAShapeLayer *> *shapeLayers;
    @end
    
    @implementation ViewController
    
    - (void)viewDidLoad {
        [super viewDidLoad];
    
        self.shapeLayers = [NSMutableArray array];
    }
    
    - (void)viewDidLayoutSubviews {
        [super viewDidLayoutSubviews];
    
        [self replaceShapeLayers];
    }
    
    - (void)replaceShapeLayers {
        // remove any old shape layers
    
        for (CAShapeLayer *shapeLayer in self.shapeLayers) {
            [shapeLayer removeFromSuperlayer];
        }
        [self.shapeLayers removeAllObjects];
    
        [self createOpenArrowPath];
    }
    
    /**
     Create open arrowhead path
    
     Note, because it's open arrowheads, we need rounded lineJoin and lineCap.
     And because both the curve and arrow head have no fill, we can use a single path for
     everything.
     */
    
    - (void)createOpenArrowPath {
        // create new curve shape layer
    
        CAShapeLayer *shapeLayer = [CAShapeLayer layer];
        shapeLayer.fillColor = [UIColor clearColor].CGColor;
        shapeLayer.strokeColor = [UIColor blueColor].CGColor;
        shapeLayer.lineWidth = 10;
        shapeLayer.lineJoin = kCALineJoinRound;
        shapeLayer.lineCap = kCALineCapRound;
    
        [self.view.layer addSublayer:shapeLayer];
    
        [self.shapeLayers addObject:shapeLayer];
    
        // now configure curve shape layer
    
        CGPoint start = CGPointMake(self.view.bounds.size.width * 0.1, self.view.bounds.size.height / 2.0);
        CGPoint end = CGPointMake(self.view.bounds.size.width * 0.9, self.view.bounds.size.height / 2.0);
        CGPoint controlPoint = CGPointMake(self.view.bounds.size.width * 0.5, self.view.bounds.size.height / 3.0);
    
        UIBezierPath *path = [UIBezierPath bezierPath];
        [path addArrowFrom:start controlPoint:controlPoint width:30 height:50 isOpen:true];
        [path addQuadCurveToPoint:end controlPoint:controlPoint];
        [path addArrowFrom:end controlPoint:controlPoint width:30 height:50 isOpen: true];
    
        shapeLayer.path = path.CGPath;
    }
    
    /**
     Create closed arrowhead path
    
     Note, because it's closed arrowheads, we need separate paths for the curve (rounded endpoints, no fill)
     and the arrowheads (mitred joins, but with fill).
     */
    
    - (void)createClosedArrowPath {
        // create new curve shape layer (with no fill and rounded corners)
    
        CAShapeLayer *shapeLayer = [CAShapeLayer layer];
        shapeLayer.fillColor = [UIColor clearColor].CGColor;
        shapeLayer.strokeColor = [UIColor blueColor].CGColor;
        shapeLayer.lineWidth = 10;
        shapeLayer.lineJoin = kCALineJoinRound;
        shapeLayer.lineCap = kCALineCapRound;
    
        [self.view.layer addSublayer:shapeLayer];
    
        [self.shapeLayers addObject:shapeLayer];
    
        // now configure curve shape layer
    
        CGPoint start = CGPointMake(self.view.bounds.size.width * 0.1, self.view.bounds.size.height / 2.0);
        CGPoint end = CGPointMake(self.view.bounds.size.width * 0.9, self.view.bounds.size.height / 2.0);
        CGPoint controlPoint = CGPointMake(self.view.bounds.size.width * 0.5, self.view.bounds.size.height / 3.0);
    
        UIBezierPath *path = [UIBezierPath bezierPath];
        [path moveToPoint:start];
        [path addQuadCurveToPoint:end controlPoint:controlPoint];
    
        shapeLayer.path = path.CGPath;
    
        // create arrow heads shape layer
    
        shapeLayer = [CAShapeLayer layer];
        shapeLayer.fillColor = [UIColor blueColor].CGColor;
        shapeLayer.strokeColor = [UIColor blueColor].CGColor;
        shapeLayer.lineWidth = 10;
        shapeLayer.lineJoin = kCALineJoinMiter;
        shapeLayer.lineCap = kCALineCapButt;
    
        [self.view.layer addSublayer:shapeLayer];
    
        [self.shapeLayers addObject:shapeLayer];
    
        // now configure curve shape layer
    
        path = [UIBezierPath bezierPath];
        [path addArrowFrom:start controlPoint:controlPoint width:30 height:50 isOpen:false];
        [path addArrowFrom:end controlPoint:controlPoint width:30 height:50 isOpen:false];
    
        shapeLayer.path = path.CGPath;
    
    }
    
    @end
    

    And that yields:

    open arrowheads

    Or

    solid filled arrowheads


    If you want to make this dynamic for any two points, just do a little trigonometry to figure out the right control point. For example, this will use a control point that is an angle that is π / 8 offset from the angle between the starting and ending points:

    - (void)createOpenArrowPathFrom:(CGPoint)start to:(CGPoint)end {
        // create new curve shape layer
    
        CAShapeLayer *shapeLayer = [CAShapeLayer layer];
        shapeLayer.fillColor = [UIColor clearColor].CGColor;
        shapeLayer.strokeColor = [UIColor blueColor].CGColor;
        shapeLayer.lineWidth = 5;
        shapeLayer.lineJoin = kCALineJoinRound;
        shapeLayer.lineCap = kCALineCapRound;
    
        [self.view.layer addSublayer:shapeLayer];
    
        [self.shapeLayers addObject:shapeLayer];
    
        // now configure curve shape layer
    
        CGFloat angle = atan2f(end.y - start.y, end.x - start.x);
        CGFloat incrementalAngle = M_PI_4 / 2.0;
        angle -= incrementalAngle;
        CGFloat distance = hypotf(end.y - start.y, end.x - start.x) / 2.0 / cosf(-incrementalAngle);
    
        CGPoint controlPoint = CGPointMake(start.x + distance * cosf(angle), start.y + distance * sinf(angle));
    
        CGFloat percent = MIN(1.0, distance / 100.0);
        UIBezierPath *path = [UIBezierPath bezierPath];
        [path addArrowFrom:start controlPoint:controlPoint width:30.0 * percent height:50.0 * percent isOpen:true];
        [path addQuadCurveToPoint:end controlPoint:controlPoint];
        [path addArrowFrom:end controlPoint:controlPoint width:30 * percent height:50 * percent isOpen: true];
    
        shapeLayer.path = path.CGPath;
    }
    

    That yields:

    enter image description here

    Clearly you can play around with this as you see fit (do it for the solid arrow, etc.), but it illustrates how to determine a plausible control point yielding a modest curve given two points.