Skip to content

Instantly share code, notes, and snippets.

@Tunous
Last active July 19, 2023 18:02
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Tunous/f891bfe7117649a76ff102b154b5c065 to your computer and use it in GitHub Desktop.
Save Tunous/f891bfe7117649a76ff102b154b5c065 to your computer and use it in GitHub Desktop.
TapPanGestureRecognizer
import SwiftUI
public class TapPanGestureRecognizer: UIGestureRecognizer {
private var waitingForSecondTap: Task<Void, Never>?
public private(set) var initialLocation: CGPoint = .zero {
didSet {
currentLocation = initialLocation
}
}
public private(set) var currentLocation: CGPoint = .zero
public override var state: UIGestureRecognizer.State {
didSet {
if state == .failed {
waitingForSecondTap?.cancel()
}
}
}
public override func reset() {
super.reset()
waitingForSecondTap = nil
initialLocation = .zero
}
public override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
super.touchesBegan(touches, with: event)
guard state == .possible else {
for touch in touches {
ignore(touch, for: event)
}
log("Gesture already in progress. Ignoring touches")
return
}
guard touches.count == 1, let touch = touches.first else {
log("Incorrect number of touches. Tap pan gesture requires only 1 touch.")
state = .failed
return
}
if touch.tapCount > 2 {
state = .failed
log("Too many taps. Tap pan gesture requires exactly 2 taps.")
return
}
if touch.tapCount == 1 {
log("Waiting for second tap")
waitingForSecondTap = Task {
do {
try await Task.sleep(nanoseconds: 380_000_000)
} catch {
return
}
log("Second tap didn't happen")
state = .failed
}
} else {
waitingForSecondTap?.cancel()
initialLocation = touch.location(in: nil)
log("Detected required tap count")
}
}
public override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent) {
super.touchesMoved(touches, with: event)
guard let touch = touches.first else {
return assertionFailure("Missing touch in call to touchesMoved")
}
switch state {
case .possible:
let currentLocation = touch.location(in: nil)
let movedDistance = currentLocation.distance(to: initialLocation)
if movedDistance > 10 {
if touch.tapCount == 2 {
state = .began
initialLocation = currentLocation
log("Recognized pan from second tap. Starting gesture.")
return
}
state = .failed
log("First tap moved too far: \(movedDistance). Failing gesture.")
return
}
case .began:
log("Starting zoom")
break
case .changed:
log("Zooming")
currentLocation = touch.location(in: nil)
default:
break
}
}
public override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent) {
super.touchesEnded(touches, with: event)
let touch = touches.first!
if touch.tapCount != 1 {
if state == .possible {
state = .cancelled
log("Zoom cancelled")
} else {
state = .ended
log("Zoom ended")
}
return
}
log("Touch with tap count \(touch.tapCount) ended")
}
private func log(_ message: @autoclosure () -> String) {
//print("[TapPanGesture] \(message())")
}
public func translation() -> CGPoint {
return CGPoint(x: initialLocation.x - currentLocation.x, y: initialLocation.y - currentLocation.y)
}
public func scale(maximumZoomFactor: CGFloat, minimumZoomFactor: CGFloat) -> CGFloat {
return scale(zoomFactor: maximumZoomFactor / minimumZoomFactor)
}
public func scale(zoomFactor: CGFloat) -> CGFloat {
let movedDistance = translation().y
let movedPercent = abs(movedDistance) / (view?.frame.height ?? 1)
let scalePercent = 1 + (zoomFactor - 1) * movedPercent
return movedDistance > 0 ? scalePercent : 1/scalePercent
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment