You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

SwiftUI中如何将DragGesture的值转换为CGPoint以正确更新星星位置?

SwiftUI中如何将DragGesture的值转换为CGPoint以正确更新星星位置?

嗨,我一眼就看出问题出在哪啦!你现在的拖拽位移计算逻辑有个小bug——DragGesture的translation是从拖拽开始点到当前位置的总偏移量,不是每次onChanged回调的增量。你现在每次都把这个总偏移量累加到当前点的坐标上,导致星星移动的距离会是你实际拖拽距离的好几倍,自然就和你的拖拽动作对不上啦。

举个例子:你拖拽星星移动20pt,第一次onChanged时translation是(10,10),你把点的x加10;第二次onChanged时translation变成(20,20),你又加20,这样点实际移动了30pt,而你只拖拽了20pt,位移当然就不匹配了。

星星拖拽示意图

下面给你两种解决方案,你可以根据自己的需求选择:

方案一:保持DrawStarView简洁,父视图处理拖拽逻辑

这种方法让DrawStarView只负责传递拖拽事件,所有位置计算都在父视图完成,符合你“让StarView尽量最小”的需求。

首先定义一个枚举来传递拖拽的不同阶段:

enum DragPhase {
    case started(initialPosition: CGPoint)
    case changed(translation: CGSize)
    case ended
}

然后修改DrawStarView,让它把拖拽的开始、变化、结束事件传递出去:

struct DrawStarView: View {
    
    init(customPoint: CustomPoint, action: @escaping (DragPhase) -> Void) {
        self.customPoint = customPoint
        self.action = action
    }
    
    let customPoint: CustomPoint
    let action: (DragPhase) -> Void
    
    var body: some View {
        createStarPath(at: customPoint.cgPointValue)
            .fill(Color.white)
            .shadow(radius: 5)
            .gesture(
                DragGesture(minimumDistance: 0)
                    .onStart { _ in
                        action(.started(initialPosition: customPoint.cgPointValue))
                    }
                    .onChanged { value in
                        action(.changed(translation: value.translation))
                    }
                    .onEnded { _ in
                        action(.ended)
                    }
            )
    }

    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
    }
}

最后修改父视图ContentView,添加初始位置的记录,用初始位置+总偏移量来计算新坐标:

struct ContentView: View {
    
    @State private var points: [CustomPoint] = [CustomPoint(x: 100, y: 100), CustomPoint(x: 200, y: 200), CustomPoint(x: 300, y: 100)]
    // 保存每个拖拽的初始位置
    @State private var dragInitialPositions: [UUID: CGPoint] = [:]
    
    var body: some View {
        GeometryReader { proxy in
            
            Color.blue
            
            ForEach($points) { $point in
                
                DrawStarView(customPoint: point) { phase in
                    switch phase {
                    case .started(let initialPos):
                        // 拖拽开始时记录星星的初始位置
                        dragInitialPositions[point.id] = initialPos
                    case .changed(let translation):
                        guard let initialPos = dragInitialPositions[point.id] else { return }
                        // 用初始位置 + 总偏移量计算新坐标,避免重复累加
                        point.cgPointValue = CGPoint(
                            x: initialPos.x + translation.width,
                            y: initialPos.y + translation.height
                        )
                    case .ended:
                        // 拖拽结束后清除记录
                        dragInitialPositions[point.id] = nil
                    }
                }
                
            }
            
        }
        .padding()
    }
}

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

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

方案二:更简洁的绑定方式(父视图代码更干净)

如果你觉得上面的枚举有点繁琐,也可以让DrawStarView直接接收绑定的CustomPoint,内部处理初始位置和拖拽逻辑,这样父视图的代码会非常清爽:

修改后的DrawStarView:

struct DrawStarView: View {
    
    @Binding var customPoint: CustomPoint
    // 保存拖拽开始时的初始位置
    @State private var initialPosition: CGPoint?
    
    var body: some View {
        createStarPath(at: customPoint.cgPointValue)
            .fill(Color.white)
            .shadow(radius: 5)
            .gesture(
                DragGesture(minimumDistance: 0)
                    .onStart { _ in
                        initialPosition = customPoint.cgPointValue
                    }
                    .onChanged { value in
                        guard let initialPos = initialPosition else { return }
                        customPoint.cgPointValue = CGPoint(
                            x: initialPos.x + value.translation.width,
                            y: initialPos.y + value.translation.height
                        )
                    }
                    .onEnded { _ in
                        initialPosition = nil
                    }
            )
    }

    // createStarPath函数和方案一一致,此处省略
}

对应的ContentView的ForEach部分简化为:

ForEach($points) { $point in
    DrawStarView(customPoint: $point)
}

这个方案的逻辑和第一种完全一样,只是把初始位置的记录放到了DrawStarView内部,父视图的代码更简洁。

这两种方案都能完美解决你现在的问题,让星星的移动距离和你的拖拽动作完全匹配~

备注:内容来源于stack exchange,提问作者swiftPunk

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.15 09:38:07