swiftui

Is it possible to detect which View currently falls under the location of a DragGesture?


I would like to be able to detect if there are any views that fall under the location of a DragGesture.

I have tried adding DragGesture to each view (circle in this case) hoping the action would transfer across views for a continuous DragGesture motion. But that didn't work.

Surely there is a way?

Screenshot

import SwiftUI

struct ContentView2: View {
    
    var body: some View {
        ZStack {
            Image(.background)
                .resizable()
                .scaledToFill()
                .ignoresSafeArea()
                
                .gesture(
                    DragGesture(minimumDistance: 5)
                        .onChanged { value in
                        
                        print ("DG detetected at \(value.location.x) : \(value.location.y)")
                        })
                
            HStack {
                Circle()
                    .fill(.red)
                    .frame(width: 100, height: 100)
                    .gesture(
                        DragGesture(minimumDistance: 0)
                            .onChanged { value in
                            print ("DG in red")
                        })
            Circle()
                .fill(.white)
                .frame(width: 100, height: 100)
            Circle()
                .fill(.blue)
                .frame(width: 100, height: 100)
            }
        }
        
    }
    
    
}

#Preview {
    ContentView2()
}

Solution

  • I would suggest saving the drag location in a GestureState variable. Then you can use a GeometryReader behind each of the circles to detect whether the drag location is inside the circle.

    struct ContentView: View {
        @GestureState private var dragLocation = CGPoint.zero
        @State private var dragInfo = " "
    
        private func dragDetector(for name: String) -> some View {
            GeometryReader { proxy in
                let frame = proxy.frame(in: .global)
                let isDragLocationInsideFrame = frame.contains(dragLocation)
                let isDragLocationInsideCircle = isDragLocationInsideFrame &&
                    Circle().path(in: frame).contains(dragLocation)
                Color.clear
                    .onChange(of: isDragLocationInsideCircle) { oldVal, newVal in
                        if dragLocation != .zero {
                            dragInfo = "\(newVal ? "entering" : "leaving") \(name)..."
                        }
                    }
            }
        }
    
        var body: some View {
            ZStack {
                Color(white: 0.2)
                VStack(spacing: 50) {
                    Text(dragInfo)
                        .foregroundStyle(.white)
                    HStack {
                        Circle()
                            .fill(.red)
                            .frame(width: 100, height: 100)
                            .background { dragDetector(for: "red") }
                        Circle()
                            .fill(.white)
                            .frame(width: 100, height: 100)
                            .background { dragDetector(for: "white") }
                        Circle()
                            .fill(.blue)
                            .frame(width: 100, height: 100)
                            .background { dragDetector(for: "blue") }
                    }
                }
            }
            .gesture(
                DragGesture(coordinateSpace: .global)
                    .updating($dragLocation) { val, state, trans in
                        state = val.location
                    }
                    .onEnded { val in
                        dragInfo = " "
                    }
            )
        }
    }
    

    For versions of iOS before iOS 17, you would need to use the version of .onChange that only takes 1 parameter (just omit the parameter oldVal).

    Animation