iosswiftswiftuigesture

How to detect when finger "exits" element bounds during drag gesture?


I am working on a custom button component for which I need to use drag gesture to set various states of custom animation, below is simplified code of where I am at the moment. Problem here is that action associated to the button gets executed during .onEnded phase. This means that user can drag their finger outside of the button and upon releasing it action will be called. I'd like to detect if finger is in bounds of the button somehow and if user drags it out, not execute action and toggle taping state to off, essentially canceling the tap when finger is not on the button.

I tried wrapping GeometryReader around my HStack, but it reported weird values (much higher values than what the real size of the button is) and it made button component expand to full screen width and height.

    HStack {
      Text("Sample Text")
    }
    .scaleEffect(taping ? 0.9 : 1)
    .gesture(
      DragGesture(minimumDistance: 0)
        .onChanged { pressed in
          taping.toggle()
        }
        .onEnded { pressed in
          taping.toggle()
          // Execute tap action here
        }
    )

Solution

  • I am able to detect if the finger is outside of the element or not using GeometryReader, but can't figure out how to forcefully end drag gesture. Please try below codes.

    Using PreferenceKey to get view's frame:

    struct ViewPreferenceKey: PreferenceKey {
        typealias Value = CGRect
    
        static var defaultValue: CGRect = CGRect()
    
        static func reduce(value: inout CGRect, nextValue: () -> CGRect) {
            value = nextValue()
        }
    }
    

    Checking drag location using GeometryReader:

    HStack {
        Text("Sample Text")
            .border(Color.black, width: 1)
    }
    .scaleEffect(taping ? 0.9 : 1)
    .background(
        GeometryReader { reader in
            let frame = reader.frame(in: .local)
            Rectangle()
                .fill(Color.clear)
                .preference(key: ViewPreferenceKey.self, value: frame)
        }
    )
    .onPreferenceChange(ViewPreferenceKey.self) { value in
        bounds = value
        print("\(bounds)")
    }
    .gesture(
        DragGesture(minimumDistance: 0)
            .onChanged { pressed in
                if bounds.contains(pressed.location) {
                    print("Inside")
                } else {
                    print("Outside")
                }
                taping = true
            }
            .onEnded { _ in
                taping = false
            }
    )
    

    You may manually invalidate the gesture once drag location is outside the view:

    DragGesture(minimumDistance: 0)
        .onChanged { pressed in
            if bounds.contains(pressed.location) && gestureInvalidated == false {
                print("Inside")
                taping = true
            } else {
                print("Outside")
                taping = false
                gestureInvalidated = true
            }
        }
        .onEnded { _ in
            taping = false
            gestureInvalidated = false
        }