swiftswiftuigesturehstackdraggesture

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


Solution

  • 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