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.