Masashi
Masashi

Reputation: 350

How to draw a directional arrow head

I have lines in many directions and angles, I draw them using UIBezierpath.

I need to draw an arrow on one of the ends of the line; dynamically depending on given point.

Edit: hand drawn but should give you a better understandin

Edit 2: with Jake answer here is my code

let y2 = line.point2Y

let path = UIBezierPath()

  path.move(to: CGPoint(x: x1, y: y1))
  path.addLine(to: CGPoint(x: x2, y: y2))
path.addArrow(start: CGPoint(x: x1, y: y1), end: CGPoint(x: x2, y: y2), pointerLineLength: ...
path.close()

let shape = CAShapeLayer()
let shapeBorder = CAShapeLayer()
shapeBorder.strokeColor = UIColor.black.cgColor
shapeBorder.lineJoin = kCALineJoinRound
shapeBorder.lineCap = kCALineCapRound
shapeBorder.lineWidth = 10
shapeBorder.addSublayer(shape)

shape.path = path.cgPath
shapeBorder.path = shape.path
shape.lineJoin = kCALineJoinRound
shape.lineCap = kCALineCapRound
shape.lineWidth = shapeBorder.lineWidth-5.0
shape.strokeColor = color

But there is a shadow enter image description here

Upvotes: 4

Views: 10908

Answers (2)

Marcin Żmigrodzki
Marcin Żmigrodzki

Reputation: 393

Great example, thanks for posting @Jake. Small update, with Xcode 12 it should be:

override func viewDidAppear(_ animated: Bool) {
    super.viewDidAppear(animated)

    let arrow = UIBezierPath()
    arrow.addArrow(start: CGPoint(x: 200, y: 200), end: CGPoint(x: 50, y: 50), pointerLineLength: 30, arrowAngle: CGFloat(Double.pi / 4))

    let arrowLayer = CAShapeLayer()
    arrowLayer.strokeColor = UIColor.black.cgColor
    arrowLayer.lineWidth = 3
    arrowLayer.path = arrow.cgPath
    arrowLayer.fillColor = UIColor.clear.cgColor
    arrowLayer.lineJoin = CAShapeLayerLineJoin.round
    arrowLayer.lineCap = CAShapeLayerLineCap.round
    self.view.layer.addSublayer(arrowLayer)
}

Upvotes: 1

Jake
Jake

Reputation: 13771

This works for me:

extension UIBezierPath {
    func addArrow(start: CGPoint, end: CGPoint, pointerLineLength: CGFloat, arrowAngle: CGFloat) {
        self.move(to: start)
        self.addLine(to: end)

        let startEndAngle = atan((end.y - start.y) / (end.x - start.x)) + ((end.x - start.x) < 0 ? CGFloat(Double.pi) : 0)
        let arrowLine1 = CGPoint(x: end.x + pointerLineLength * cos(CGFloat(Double.pi) - startEndAngle + arrowAngle), y: end.y - pointerLineLength * sin(CGFloat(Double.pi) - startEndAngle + arrowAngle))
        let arrowLine2 = CGPoint(x: end.x + pointerLineLength * cos(CGFloat(Double.pi) - startEndAngle - arrowAngle), y: end.y - pointerLineLength * sin(CGFloat(Double.pi) - startEndAngle - arrowAngle))

        self.addLine(to: arrowLine1)
        self.move(to: end)
        self.addLine(to: arrowLine2)
    }
}

class MyViewController : UIViewController {
    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)

        let arrow = UIBezierPath()
        arrow.addArrow(start: CGPoint(x: 200, y: 200), end: CGPoint(x: 50, y: 50), pointerLineLength: 30, arrowAngle: CGFloat(Double.pi / 4))

        let arrowLayer = CAShapeLayer()
        arrowLayer.strokeColor = UIColor.black.cgColor
        arrowLayer.lineWidth = 3
        arrowLayer.path = arrow.cgPath
        arrowLayer.fillColor = UIColor.clear.cgColor
        arrowLayer.lineJoin = kCALineJoinRound
        arrowLayer.lineCap = kCALineCapRound
        self.view.layer.addSublayer(arrowLayer)
    }
}

enter image description here

EDIT

Also, make sure you set your the fillColor of your CAShapeLayer to UIColor.clear.cgColor. Otherwise, your layer will fill in the area between the start point of the arrow and one of the lines at the end.

Upvotes: 19

Related Questions