How to build a DragGesture with a "sticky" behavior in SwifUI?

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:

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?

A iphone screen if some Rectangles

Upvotes: 0

Views: 160

Answers (1)

Benzy Neez
Benzy Neez

Reputation: 20992

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

Animation

Upvotes: 3

Related Questions