Last active
July 19, 2023 18:02
-
-
Save Tunous/f891bfe7117649a76ff102b154b5c065 to your computer and use it in GitHub Desktop.
TapPanGestureRecognizer
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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