swiftswiftui

How to convert dragGesture value to CGPoint in SwiftUI?


I have an array of defined points, and I'm using these points to draw a star at each location. The star view should simply report back the drag gesture for updating each point's position. Currently, the code works partially: the drag amount is not equal to the displacement of the stars.

My goal is to keep the star view as minimal as possible; I only want to use this view to read the drag amount. I would like the points' positions to be updated within a ForEach loop. How can I correctly convert the DragGesture value to a CGPoint in the ForEach to achieve this?

enter image description here

import SwiftUI

struct ContentView: View {
    
    @State private var points: [CustomPoint] = [CustomPoint(x: 100, y: 100), CustomPoint(x: 200, y: 200), CustomPoint(x: 300, y: 100)]
    
    var body: some View {
        GeometryReader { proxy in
            
            Color.blue
            
            ForEach($points) { $point in
                
                DrawStarView(customPoint: point) { value in
                    point.cgPointValue = CGPoint(x: point.cgPointValue.x + value.width, y: point.cgPointValue.y + value.height)
                }
                
            }
            
            
        }
        .padding()
    }
}

struct CustomPoint: Identifiable {
    
    init(x: CGFloat, y: CGFloat) {
        self.cgPointValue = CGPoint(x: x, y: y)
    }

    let id: UUID = UUID()
    var cgPointValue: CGPoint
}


struct DrawStarView: View {
    
    init(customPoint: CustomPoint, action: @escaping (CGSize) -> Void) {
        self.customPoint = customPoint
        self.action = action
    }
    
    let customPoint: CustomPoint
    let action: (CGSize) -> Void
    
    
    var body: some View {
                
        return createStarPath(at: customPoint.cgPointValue)
            .fill(Color.white)
            .shadow(radius: 5)
            .gesture(dragGesture)
        
    }

    func createStarPath(at point: CGPoint, size: CGSize = CGSize(width: 100, height: 100)) -> Path {
        let outerRadius = size.width / 2
        let innerRadius = outerRadius / 2.5
        let center = point
        let pointsOnStar = 5
        let angleIncrement = .pi * 2 / CGFloat(pointsOnStar * 2)
        
        var path = Path()
        
        for i in 0..<pointsOnStar * 2 {
            let angle = angleIncrement * CGFloat(i) - .pi / 2
            let radius = (i % 2 == 0) ? outerRadius : innerRadius
            let x = center.x + radius * cos(angle)
            let y = center.y + radius * sin(angle)
            
            if i == 0 {
                path.move(to: CGPoint(x: x, y: y))
            } else {
                path.addLine(to: CGPoint(x: x, y: y))
            }
        }
        
        path.closeSubpath()
        return path
    }

    private var dragGesture: some Gesture {
        
        DragGesture(minimumDistance: 0)
            .onChanged { value in
                action(value.translation)
            }
            .onEnded { value in
                action(value.translation)
            }
        
    }

}

Solution

  • translation is the total translation since the gesture has started. It is not the difference between the location between this call to onChange and the next call to onChange.

    If the difference is what you want, you should track the previous translation using a separate @State, and subtract that from the current translation, and pass that into action.

    @State var lastTranslation: CGSize = .zero
    
    private var dragGesture: some Gesture {
        
        DragGesture(minimumDistance: 0)
            .onChanged { value in
                action(.init(
                    width: value.translation.width - lastTranslation.width,
                    height: value.translation.height - lastTranslation.height)
                )
                lastTranslation = value.translation
            }
            .onEnded { value in
                action(.init(
                    width: value.translation.width - lastTranslation.width,
                    height: value.translation.height - lastTranslation.height)
                )
                lastTranslation = .zero
            }
        
    }