Search code examples
swiftuiuigesturerecognizercgpointcgpath

How to make a Line follow a CGPoint in SwiftUI?


I've created two CGPoints and a connecting line between them. My aim is the following: When I drag the second CGPoint with a SwiftUI DragGesture() the line will follow it (as if it's connected to it). At the moment the line doesn't follow the point. How can I do that?

enter image description here

Here's my code:

import SwiftUI

struct Point : View {
    var position: CGPoint

    init(_ position: CGPoint) {
        self.position = position
    }
    var body: some View {
        Circle()
            .frame(width: 20)
            .foregroundColor(.white)
            .position(x: position.x, y: position.y)
    }
}

LineWithPoints.swift

struct LineWithPoints : View {
    @State var point1 = Point(.init(x: 0,   y: 0))
    @State var point2 = Point(.init(x: 450, y: 0))
    @State private var offset1 = CGSize.zero
    
    var body: some View {
        let lineBetween = LineBetween(pointPosition: $point2.position)
        
        ZStack {
            lineBetween.stroke(.red, lineWidth: 2)
            point1;               
            point2
                .offset(x: offset1.width, y: offset1.height)
                .gesture(
                    DragGesture()
                        .onChanged { value in
                            let x = value.startLocation.x
                            let y = value.startLocation.y
                            let w = value.translation.width
                            let h = value.translation.height
                            offset1 = .init(width: (x + w) - point2.position.x,
                                           height: (y + h) - point2.position.y)
                        }
                )
        }
    }
    struct LineBetween : Shape, @unchecked Sendable {
        @Binding var pointPosition: CGPoint

        func path(in rect: CGRect) -> Path {
            var path = Path()
            path.move(to: CGPoint.zero)
            path.addLine(to: pointPosition)
            path.closeSubpath()
            return path
        }
    }
}

ContentView.swift

struct ContentView : View {
    var body: some View {
        ZStack {
            Color.black.ignoresSafeArea()
            LineWithPoints()
        }
    }
}

Solution

  • Here are some things which could do with correction:

    • Your struct LineBetween only accepts one point as argument, it assumes the other point is at (0,0).
    • There is no need for a Binding because the point is read-only.
    • Instead of creating the shape outside the ZStack and then stroking it inside the stack, it would perhaps be better to create it and stroke it inside the ZStack in one operation.
    • The line is always being drawn to the position of point2, but this does not take the drag offset into consideration. This is the main reason why the line is not changing.
    • At end of drag, you probably want to reset the offset and update the position of point 2, ready for another drag gesture.

    Here is a revised version which addresses the points above. It draws the line to a target point. This is a computed property that combines the position of point 2 with the drag offset.

    struct LineWithPoints : View {
        @State private var point1: Point
        @State private var point2: Point
        @State private var offset = CGSize()
    
        init(
            point1: CGPoint = CGPoint(x: 50, y: 200),
            point2: CGPoint = CGPoint(x: 325, y: 200)
        ) {
            self._point1 = State(initialValue: Point(point1))
            self._point2 = State(initialValue: Point(point2))
        }
    
        struct LineBetween : Shape {
            let point1: CGPoint
            let point2: CGPoint
    
            func path(in rect: CGRect) -> Path {
                var path = Path()
                path.move(to: point1)
                path.addLine(to: point2)
                path.closeSubpath()
                return path
            }
        }
    
        private var targetPoint: CGPoint {
            CGPoint(
                x: point2.position.x + offset.width,
                y: point2.position.y + offset.height
            )
        }
    
        var body: some View {
            ZStack {
                LineBetween(
                    point1: point1.position,
                    point2: targetPoint
                )
                .stroke(.red, lineWidth: 2)
                point1
                point2
                    .offset(x: offset.width, y: offset.height)
                    .gesture(
                        DragGesture()
                            .onChanged { value in
                                offset = value.translation
                            }
                            .onEnded { _ in
                                point2 = Point(targetPoint)
                                offset = CGSize()
                            }
                    )
            }
        }
    }
    

    LineBetween