Home > Back-end >  How to draw a directional arrow head with SwiftUI?
How to draw a directional arrow head with SwiftUI?

Time:05-31

I used swiftui to implement a demo that can recognize user gestures and draw lines. Now I want it to add an arrow at the end of the line and different segments have arrows with different angles. What should I do?

This is my code

struct LineView: View {
    @State var removeAll = false
    @State var lines = [CGPoint]()
    var body: some View {
        ZStack {
            Rectangle()
                .cornerRadius(20)
                .opacity(0.1)
                .shadow(color: .gray, radius: 4, x: 0, y: 2)
            Path { path in
                path.addLines(lines)
            }
            .stroke(lineWidth: 3)
        }
        .gesture(
            DragGesture()
                .onChanged { state in
                    if removeAll {
                        lines.removeAll()
                        removeAll = false
                    }
                    
                    lines.append(state.location)
                }
                .onEnded { _ in
                    removeAll = true
                }
        )
        .frame(width: 370, height: 500)
    }
}

CodePudding user response:

There are plenty of ways to determine how to position and rotate the arrow at the end of the line. Mostly you need to have last two points which determine the arrow position and rotation.

If you already have a drawing of an arrow (a path or an image) then it may be easiest to use transform. It should look like this:

    private func arrowTransform(lastPoint: CGPoint, previousPoint: CGPoint) -> CGAffineTransform {
        let translation = CGAffineTransform(translationX: lastPoint.x, y: lastPoint.y)
        let angle = atan2(lastPoint.y-previousPoint.y, lastPoint.x-previousPoint.x)
        let rotation = CGAffineTransform(rotationAngle: angle)
        return rotation.concatenating(translation)
    }

The key here is using atan2 which is a real life saver. It is basically arctangent which does't need the extra if-statements to determine the angle between two points. It accepts delta-y and delta-x; that's all there is to know about it. With this data you can create a translation matrix and a rotation matrix which can then be concatenated to get the end result.

To apply it you simply use it like the following:

arrowPath()
    .transform(arrowTransform(lastPoint: lines[lines.count-1], previousPoint: lines[lines.count-2]))
    .fill()

I simply used a path here to draw an arrow around (0, 0) and then fill it.

The whole thing together that I used is:

struct LineView: View {
    @State var removeAll = false
    @State var lines = [CGPoint]()
    
    private func arrowPath() -> Path {
        // Doing it rightwards
        Path { path in
            path.move(to: .zero)
            path.addLine(to: .init(x: -10.0, y: 5.0))
            path.addLine(to: .init(x: -10.0, y: -5.0))
            path.closeSubpath()
        }
    }
    
    private func arrowTransform(lastPoint: CGPoint, previousPoint: CGPoint) -> CGAffineTransform {
        let translation = CGAffineTransform(translationX: lastPoint.x, y: lastPoint.y)
        let angle = atan2(lastPoint.y-previousPoint.y, lastPoint.x-previousPoint.x)
        let rotation = CGAffineTransform(rotationAngle: angle)
        return rotation.concatenating(translation)
    }
    
    var body: some View {
        ZStack {
            Rectangle()
                .cornerRadius(20)
                .opacity(0.1)
                .shadow(color: .gray, radius: 4, x: 0, y: 2)
            Path { path in
                path.addLines(lines)
            }
            .stroke(lineWidth: 3)
            if lines.count >= 2 {
                arrowPath()
                    .transform(arrowTransform(lastPoint: lines[lines.count-1], previousPoint: lines[lines.count-2]))
                    .fill()
            }
        }
        .gesture(
            DragGesture()
                .onChanged { state in
                    if removeAll {
                        lines.removeAll()
                        removeAll = false
                    }
                    
                    lines.append(state.location)
                }
                .onEnded { _ in
                    removeAll = true
                }
        )
        .frame(width: 370, height: 500)
    }
}

It still needs some work. Either removing the last point on line or moving the arrow to overlap the last line. But I think you should be able to take it from here.

  • Related