I'm trying to implement a drag and drop functionality in my app using SwiftUI.
I create two circles which are located in two different HStacks
. They do not share the same coordinate space.
The circle with the stroke is the target, the green filled circle is the object that will be dragged.
I was able to get their absolute positions using a GeometryReader
inside an .overlay
. I use this to detect if they are overlapping when the object circle is dragged over the target circle. This works.
When they don't overlap, the object circle will move back to its original position. When they do overlap the object circle is supposed to snap into place at the position of the target circle. Here is where I seem to have issues.
I am trying to set the new X and Y positions of the object circle by taking the: object circle local position - object circle global position + target circle global position.
objectPosition = CGPoint(
x: objectPosition.x - objectFrame.midX + targetFrame.midX,
y: objectPosition.y - objectFrame.midY + targetFrame.midY
)
I would assume this brings me to the target circle's coordinate space. But somehow it does not work.
So far I have been unable to find a reliable and simple way to convert coordinate spaces in SwiftUI. Using the workaround of GeometryReader
inside an overlay at least gives me the right global positions. But I haven't found a way to use those positions to place views in a .global
coordinate space as well.
If someone has an idea why my calculation of the coordinate space is wrong, or even knows a way to convert and position views relative to each other more easily I'd much appreciate it.
Here my code for a SwiftUI single view iOS app:
import SwiftUI
struct ContentView: View {
@State private var isDragging: Bool = false
@State private var objectDragOffset: CGSize = .zero
@State private var objectPosition: CGPoint = .zero
@State private var objectFrame: CGRect = .zero
@State private var targetFrame: CGRect = .zero
var body: some View {
VStack {
HStack {
Circle()
.stroke(lineWidth: 3)
.fill(Color.blue)
.frame(width: 100.0, height: 100.0)
.overlay(
GeometryReader { targetGeometry in
Color(.clear)
.onAppear { targetFrame = targetGeometry.frame(in: .global) }
}
)
.position(CGPoint(x:180, y: 190))
}.background(Color.yellow)
HStack {
Circle()
.foregroundColor(.green)
.frame(width: 100, height: 100, alignment: .center)
.overlay(
GeometryReader { objectGeometry in
Color(.clear)
.onAppear {
objectFrame = objectGeometry.frame(in: .global) }
}
)
.position(objectPosition)
.offset( isDragging ? objectDragOffset : .zero)
.onAppear { objectPosition = CGPoint(x: 200, y: 250 ) }
.gesture(
DragGesture(coordinateSpace: .global)
.onChanged { drag in
isDragging = true
objectDragOffset = drag.translation
}
.onEnded { drag in
isDragging = false
if targetFrame.contains(drag.location) {
objectPosition = CGPoint(
x: objectPosition.x - objectFrame.midX + targetFrame.midX,
y: objectPosition.y - objectFrame.midY + targetFrame.midY
)
} else {
objectPosition = CGPoint(x: 200, y: 250 )
}
}
)
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
GeometryReader
in the overlays and where the overlay is placed in the code. As using it this way is a workaround it seems either the overlay is working correctly or the GeometryReader depending where it's placed and when the .onAppear is called. I'm trying to figure out if this is the case. – Nakesha