114 lines
3.8 KiB
Swift
114 lines
3.8 KiB
Swift
//
|
|
// Copyright 2024 Signal Messenger, LLC
|
|
// SPDX-License-Identifier: AGPL-3.0-only
|
|
//
|
|
|
|
public import UIKit
|
|
|
|
public protocol SingleOrDoubleTapGestureDelegate: AnyObject {
|
|
|
|
/// A single tap was recognized; return true if handled to end the gesture immediately.
|
|
func handleSingleTap(_ sender: SingleOrDoubleTapGestureRecognizer) -> Bool
|
|
|
|
/// A double tap was recognized; return true if handled to signal to `didEndGesture`.
|
|
func handleDoubleTap(_ sender: SingleOrDoubleTapGestureRecognizer) -> Bool
|
|
|
|
/// The gesture ended in one of three ways:
|
|
/// 1. `handleSingleTap` returned true
|
|
/// 2. `handleDoubleTap` returned true OR false
|
|
/// 3. `handleSingleTap` returned false, then the gesture timed out waiting for double tap.
|
|
///
|
|
/// - parameter wasHandled: True if either `handleSingleTap` or `handleDoubleTap` returned true this gesture.
|
|
func didEndGesture(_ sender: SingleOrDoubleTapGestureRecognizer, wasHandled: Bool) -> Void
|
|
}
|
|
|
|
public class SingleOrDoubleTapGestureRecognizer: UIGestureRecognizer {
|
|
|
|
// MARK: - API
|
|
|
|
public func setTapDelegate(_ delegate: SingleOrDoubleTapGestureDelegate) {
|
|
self.tapDelegate = delegate
|
|
}
|
|
|
|
// MARK: - Private
|
|
|
|
/// The default for UITapGestureRecognizer is 0.35. This is lower because of where we use it (conversation view)
|
|
/// where we want to balance the speed of detecting a single tap with the window to actually do a double tap.
|
|
/// Roughly, >=0.3 feels too slow when intending to single tap; below 0.2 makes it impossible to double tap.
|
|
private static let maxIntervalBetweenTaps: TimeInterval = 0.2
|
|
|
|
private var numTouches: Int = 0
|
|
private var timer: Timer? {
|
|
didSet {
|
|
oldValue?.invalidate()
|
|
}
|
|
}
|
|
|
|
private var singleTapHandled: Bool = false
|
|
private var doubleTapHandled: Bool = false
|
|
|
|
private weak var tapDelegate: SingleOrDoubleTapGestureDelegate?
|
|
|
|
// MARK: - Event Handling
|
|
|
|
override public func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
|
|
super.touchesBegan(touches, with: event)
|
|
|
|
if touches.count != 1 {
|
|
state = .failed
|
|
}
|
|
}
|
|
|
|
override public func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent) {
|
|
super.touchesEnded(touches, with: event)
|
|
|
|
guard !touches.isEmpty else {
|
|
return
|
|
}
|
|
|
|
if numTouches == 0 {
|
|
numTouches += 1
|
|
self.state = .changed
|
|
self.singleTapHandled = tapDelegate?.handleSingleTap(self) ?? false
|
|
if self.singleTapHandled {
|
|
endGesture()
|
|
} else {
|
|
// If we _can_ double tap, start a timer and wait for the second tap.
|
|
timer = Timer.scheduledTimer(
|
|
withTimeInterval: Self.maxIntervalBetweenTaps,
|
|
repeats: false,
|
|
block: { [weak self] _ in
|
|
defer { self?.timer = nil }
|
|
guard let self else { return }
|
|
guard self.numTouches == 1 else {
|
|
return
|
|
}
|
|
endGesture()
|
|
}
|
|
)
|
|
}
|
|
} else if numTouches == 1 {
|
|
numTouches += 1
|
|
self.timer = nil
|
|
self.state = .recognized
|
|
self.doubleTapHandled = tapDelegate?.handleDoubleTap(self) ?? false
|
|
endGesture()
|
|
} else {
|
|
self.state = .failed
|
|
}
|
|
}
|
|
|
|
private func endGesture() {
|
|
let wasHandled = self.singleTapHandled || self.doubleTapHandled
|
|
self.state = .ended
|
|
tapDelegate?.didEndGesture(self, wasHandled: wasHandled)
|
|
}
|
|
|
|
override public func reset() {
|
|
super.reset()
|
|
numTouches = 0
|
|
singleTapHandled = false
|
|
doubleTapHandled = false
|
|
timer = nil
|
|
}
|
|
}
|