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

