1

I have 2 independent set of data, one is used to draw a line(a Path), and the other is used to put a small view somewhere on the line(Circle).

You can imagine it just like a Slider(in fact the UX should be something like that)

enter image description here

I wanted to add a Drag Gesture on the Red Circle View, so that the user can slide it any where between the line end points. But the way i've implemented or thought it to be isn't working at all or is pretty bad.

The know set of data is:

  • Start of the Line - CGPoint

  • End of the Line - CGPoint

  • Position of the Circle View - CGPoint

For simplicity of the example, i've added some simple points namely Top & Bottom, but in reality, what i'm trying to achieve is, establish a perimeter on a Map Layer, by fetching the Lat/Long Coordinates, converting it to Screen Coordinates space and plotting a Path(line) for those 2 points and thereafter giving the ability to the user to label the Perimeter and giving the freedom to drag it along the perimeter line as per the user's need. (One label per line, no complications on that).

That being said, Slider wasn't an option.

Which looks something like:
enter image description here

Sample Code:

struct TestView: View {
    @State var position = CGPoint(x: 60, y: 60)

    let top = CGPoint(x: 50, y: 50)
    let bottom = CGPoint(x: 300, y: 300)

    var body: some View {
        ZStack {
            Path { path in
                path.move(to: self.top)
                path.addLine(to: self.bottom)
            }
            .stroke(Color.red, lineWidth: 5)

            Circle()
                .foregroundColor(.red)
                .frame(width: 20)
                .position(self.position)
                .gesture(
                    DragGesture(minimumDistance: 0, coordinateSpace: .global)
                        .onChanged { drag in
                            print(drag.location)
                            if
                                self.top.x <= drag.location.x,
                                self.bottom.x >= drag.location.x,
                                self.top.y <= drag.location.y,
                                self.bottom.y >= drag.location.y,
                                self.pointOnLine(point: drag.location)
                            {                               
                                self.position = drag.location
                            }
                        }
                )
        }
    }   
}

Helper Method to check if point is on line:

func pointOnLine(point: CGPoint) -> Bool {
        let dxc = point.x - top.x
        let dyc = point.y - top.y

        let dxl = bottom.x - top.x
        let dyl = bottom.y - top.y

        let cross = dxc * dyl - dyc * dxl
        return cross == 0
    }

Any help appreciated. Thanks in advance.

Pulkit Vaid
  • 103
  • 1
  • 8

2 Answers2

1

Got it Working

Updated the helper method to get the closest intersection point of drag location and a point on line.

Assigned in to self.position which keeps the View sticking to the line.

Gesture:

DragGesture(minimumDistance: 0, coordinateSpace: .global)
                        .onChanged { drag in
                            self.position = self.pointFrom(drag.location,
                                                           toLineSegment: self.top, self.bottom)
                        }

Helper Method to get Point on Line:

private func pointFrom(_ point: CGPoint, toLineSegment start: CGPoint, _ end: CGPoint) -> CGPoint {
        let pointAndStartXDiff = point.x - start.x
        let pointAndStartYDiff = point.y - start.y
        let startAndEndXDiff = end.x - start.x
        let startAndEndYDiff = end.y - start.y

        let dotProduct = pointAndStartXDiff * startAndEndXDiff + pointAndStartYDiff * startAndEndYDiff
        let lengthSquare = startAndEndXDiff * startAndEndXDiff + startAndEndYDiff * startAndEndYDiff
        let param = dotProduct / lengthSquare

        // intersection of normal to start, end that goes through point
        var xIntersection, yIntersection: CGFloat

        if param < 0 || (start.x == end.x && start.y == end.y) {
            xIntersection = start.x
            yIntersection = start.y
        } else if param > 1 {
            xIntersection = end.x
            yIntersection = end.y
        } else {
            xIntersection = start.x + param * startAndEndXDiff
            yIntersection = start.y + param * startAndEndYDiff
        }

        return CGPoint(x: xIntersection, y: yIntersection)
    }

Rest everything is same.

Results in something like this:

enter image description here

Pulkit Vaid
  • 103
  • 1
  • 8
0

You could try using position along the line, that is, using the angle of the line.

you have some sort of drag view to get the current position:

var drag: some Gesture {
    DragGesture().onChanged { value in self.pos = CGPoint(x: value.location.x, y: value.location.y)}
}

you know the initial and final positions of the line, so you know the angle "atan(dx,dy)". So staring from the start position you can get the next position for the circle, something like x = dcos(angle)+x0, y=dsin(angle)+y0 ...

  • thanks for the help @workingdog, but if you could please explain a bit more. How can I make use of the Drag's Location point, and make that Circle stick to the line while dragging up and down – Pulkit Vaid Apr 12 '20 at 20:39