// 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 } } }