I am building a simple drag gesture animation in a Rectangle() that, in the end of the gesture, stick the shape in the closest item of a group.
Check the example in the image bellow.
I was able to achieve a decent result with the following approach:
Manually computing the x absolute position of the black rectangles:
@State private var xPos : [Int] = [40,110,180,250,320]
Placing the rectangles in the screen with a absolute position property:
Rectangle()
.position(x: CGFloat(xPos[i]), y: 0)
Finally, in the blue Rectangle, add the gesture property, with a fancy func on .onEnded to find the closest element, and apply the x position accordingly.
Rectangle()
.fill(.blue)
.position(location)
.gesture(
DragGesture()
.onChanged { gesture in
location.x = gesture.location.x
}
.onEnded { value in
withAnimation(.spring()) {
var goal = Int(value.location.x)
var closest = xPos[0]
var index = 0
for item in xPos {
var distanceToActual = abs(closest - goal)
if (xPos.count - 1 > index) { // prevent out of the bound
var distanceToNext = abs(xPos[index + 1] - goal)
if(distanceToNext < distanceToActual) {
closest = xPos[index + 1]
}
}
index += 1
}
location.x = CGFloat(closest)
}
}
My question is, is there a more elegant way of doing it? It's very annoying to manually calc the position of the Black rectangles... Is there any way of doing this with the Black Rectangles placed in a HStack instead of manually position then?
The technique described in the answer to Is it possible to detect which View currently falls under the location of a DragGesture? can be used to detect, which of the squares is closest to the drag position (it was my answer).
matchedGeometryEffect
then provides a convenient way to match the position of the blue rectangle to the identified square.
Like this:
@State private var dragLocation = CGPoint.zero
@State private var indexForDragLocation = 0
@Namespace private var ns
private func dragDetector(for index: Int) -> some View {
GeometryReader { proxy in
let width = proxy.size.width
let midX = proxy.frame(in: .global).midX
let dx = abs(midX - dragLocation.x)
let isClosest = dx < (width / 2)
Color.clear
// pre iOS 17: .onChange(of: isClosest) { newVal in
.onChange(of: isClosest) { oldVal, newVal in
if newVal {
indexForDragLocation = index
}
}
}
}
var body: some View {
HStack(spacing: 0) {
ForEach(0...4, id: \.self) { index in
Color(white: 0.2)
.frame(width: 20, height: 20)
.padding(.horizontal, 25)
.matchedGeometryEffect(
id: index,
in: ns,
isSource: index == indexForDragLocation
)
.background {
dragDetector(for: index)
}
}
}
.background {
RoundedRectangle(cornerRadius: 4)
.fill(.blue)
.frame(width: 40, height: 80)
.matchedGeometryEffect(
id: indexForDragLocation,
in: ns,
properties: .position,
isSource: false
)
.animation(.spring, value: indexForDragLocation)
.gesture(
DragGesture(coordinateSpace: .global)
.onChanged { val in
dragLocation = val.location
}
)
}
}