Force a custom gesture to end in SwiftUI

Viewed 53

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.

0 Answers
Related