'Force a custom gesture to end in SwiftUI

I've implemented a custom gesture via the Gesture protocol and it is mostly working. The one thing I have yet to figure out is how to declare that the gesture has completed.

Here is my current code.

import Foundation
import SwiftUI
import CoreGraphics


struct WiggleGesture: Gesture {
    
    struct WiggleState {
        var startTime: Date?
        var bounces: Int = 0
        var lastLocation = CGPoint.zero
        var lastBounceLocation = CGPoint.zero
    }
    
    struct WiggleValue {
        var bounces: Int
        var elapsedTime: TimeInterval?
    }
    
    typealias Value = WiggleValue
    
    @GestureState var wiggleState = WiggleState()

    let distanceThreshold: CGFloat
    let angleThreshold: CGFloat

    init(distanceThreshold: CGFloat = 15, angleThreshold: CGFloat = 120) {
        self.distanceThreshold = distanceThreshold
        self.angleThreshold = angleThreshold
    }

    var body: AnyGesture<Value> {
        AnyGesture (
            DragGesture(minimumDistance: distanceThreshold, coordinateSpace: .global)
                .updating($wiggleState) { (value, state, _) in
                    if state.startTime == nil {
                        state.startTime = Date.now
                    }
                    
                    let currentSwipe = value.translation.asVector() - state.lastBounceLocation.asVector()  // The current broad motion, from the last bounce location.
                    let currentTrajectory = value.translation.asVector() - state.lastLocation.asVector()  // The current immediate motion, from the last tick.
                    
                    //TODO: Consider if there should be a minimum translation before counting a bounce?
                    let trajectoryDelta = abs(currentSwipe.angleTo(other: currentTrajectory))
                    if abs(trajectoryDelta) > angleThreshold {
                        state.bounces += 1
                        state.lastBounceLocation = value.translation.asPoint()
                    }
                    
                    state.lastLocation = value.translation.asPoint()
                }
                .map { _ in
                    var elapsedTime: TimeInterval?
                    if let startTime = wiggleState.startTime {
                        elapsedTime = Date.now.timeIntervalSince(startTime)
                    }
                    return WiggleValue(bounces: wiggleState.bounces, elapsedTime: elapsedTime)
                }
        )
    }
}

extension View {
    func onWiggle(distanceThreshold: CGFloat = 15, angleThreshold: CGFloat = 120, perform action: @escaping (WiggleGesture.Value) -> Void) -> some View {
        gesture(
            WiggleGesture(distanceThreshold: distanceThreshold, angleThreshold: angleThreshold)
                .onEnded { value in
                    action(value)
                }
        )
    }
}

Currently, .onEnded is still the one that comes from DragGesture and triggers when the user releases their touch.

Ideally, what I want is to specify a "bounces required" and "timeout" to the gesture so that when it has detected n number of bounces within the timeout it will trigger the .onEnded call and the gesture will stop evaluating.



Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source