session-ios/Session/Media Viewing & Editing/Transitions/MediaInteractiveDismiss.swift

128 lines
4.6 KiB
Swift

// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import UIKit
import SignalUtilitiesKit
// MARK: - InteractivelyDismissableViewController
protocol InteractivelyDismissableViewController: UIViewController {
func performInteractiveDismissal(animated: Bool)
}
// MARK: - InteractiveDismissDelegate
protocol InteractiveDismissDelegate: AnyObject {
func interactiveDismissUpdate(_ interactiveDismiss: UIPercentDrivenInteractiveTransition, didChangeTouchOffset offset: CGPoint)
func interactiveDismissDidFinish(_ interactiveDismiss: UIPercentDrivenInteractiveTransition)
}
// MARK: - MediaInteractiveDismiss
class MediaInteractiveDismiss: UIPercentDrivenInteractiveTransition {
var interactionInProgress = false
weak var interactiveDismissDelegate: InteractiveDismissDelegate?
private weak var targetViewController: InteractivelyDismissableViewController?
init(targetViewController: InteractivelyDismissableViewController) {
super.init()
self.targetViewController = targetViewController
}
public func addGestureRecognizer(to view: UIView) {
let gesture: DirectionalPanGestureRecognizer = DirectionalPanGestureRecognizer(direction: .vertical, target: self, action: #selector(handleGesture(_:)))
// Allow panning with trackpad
if #available(iOS 13.4, *) { gesture.allowedScrollTypesMask = .continuous }
view.addGestureRecognizer(gesture)
}
// MARK: - Private
private var fastEnoughToCompleteTransition = false
private var farEnoughToCompleteTransition = false
private var lastProgress: CGFloat = 0
private var lastIncreasedProgress: CGFloat = 0
private var shouldCompleteTransition: Bool {
if farEnoughToCompleteTransition { return true }
if fastEnoughToCompleteTransition { return true }
return false
}
@objc private func handleGesture(_ gestureRecognizer: UIScreenEdgePanGestureRecognizer) {
guard let coordinateSpace = gestureRecognizer.view?.superview else { return }
if case .began = gestureRecognizer.state {
gestureRecognizer.setTranslation(.zero, in: coordinateSpace)
}
let totalDistance: CGFloat = 100
let velocityThreshold: CGFloat = 500
switch gestureRecognizer.state {
case .began:
interactionInProgress = true
targetViewController?.performInteractiveDismissal(animated: true)
case .changed:
let velocity = abs(gestureRecognizer.velocity(in: coordinateSpace).y)
if velocity > velocityThreshold {
fastEnoughToCompleteTransition = true
}
let offset = gestureRecognizer.translation(in: coordinateSpace)
let progress = abs(offset.y) / totalDistance
// `farEnoughToCompleteTransition` is cancelable if the user reverses direction
farEnoughToCompleteTransition = (progress >= 0.5)
// If the user has reverted enough progress then we want to reset the velocity
// flag (don't want the user to start quickly, slowly drag it back end end up
// dismissing the screen)
if (lastIncreasedProgress - progress) > 0.2 || progress < 0.05 {
fastEnoughToCompleteTransition = false
}
update(progress)
lastIncreasedProgress = (progress > lastProgress ? progress : lastIncreasedProgress)
lastProgress = progress
interactiveDismissDelegate?.interactiveDismissUpdate(self, didChangeTouchOffset: offset)
case .cancelled:
interactiveDismissDelegate?.interactiveDismissDidFinish(self)
cancel()
interactionInProgress = false
farEnoughToCompleteTransition = false
fastEnoughToCompleteTransition = false
lastIncreasedProgress = 0
lastProgress = 0
case .ended:
if shouldCompleteTransition {
finish()
}
else {
cancel()
}
interactiveDismissDelegate?.interactiveDismissDidFinish(self)
interactionInProgress = false
farEnoughToCompleteTransition = false
fastEnoughToCompleteTransition = false
lastIncreasedProgress = 0
lastProgress = 0
default:
break
}
}
}