// // Copyright (c) 2020 Touch Instinct // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the Software), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. // import UIKit open class TransitionDriver: UIPercentDrivenInteractiveTransition { private weak var presentedController: UIViewController? private var panRecognizer: UIPanGestureRecognizer? public var direction: TransitionDirection = .present // MARK: - Linking public func link(to controller: UIViewController) { presentedController = controller let panGesture = UIPanGestureRecognizer(target: self, action: #selector(handleGesture)) panRecognizer = panGesture presentedController?.view.addGestureRecognizer(panGesture) } // MARK: - Override override open var wantsInteractiveStart: Bool { get { switch direction { case .present: return false case .dismiss: return panRecognizer?.state == .began } } set {} } @objc private func handleGesture(recognizer: UIPanGestureRecognizer) { switch direction { case .present: handlePresentation(recognizer: recognizer) case .dismiss: handleDismiss(recognizer: recognizer) } } } // MARK: - Gesture Handling private extension TransitionDriver { var maxTranslation: CGFloat { presentedController?.view.frame.height ?? 0 } /// `pause()` before call `isRunning` var isRunning: Bool { percentComplete != 0 } func handlePresentation(recognizer: UIPanGestureRecognizer) { switch recognizer.state { case .began: pause() case .changed: update(percentComplete - recognizer.incrementToBottom(maxTranslation: maxTranslation)) case .ended, .cancelled: if recognizer.isProjectedToDownHalf(maxTranslation: maxTranslation) { cancel() } else { finish() } case .failed: cancel() default: break } } func handleDismiss(recognizer: UIPanGestureRecognizer) { switch recognizer.state { case .began: pause() // Pause allows to detect isRunning if !isRunning { presentedController?.dismiss(animated: true) // Start the new one } case .changed: update(percentComplete + recognizer.incrementToBottom(maxTranslation: maxTranslation)) case .ended, .cancelled: if recognizer.isProjectedToDownHalf(maxTranslation: maxTranslation) { finish() } else { cancel() } case .failed: cancel() default: break } } } private extension UIPanGestureRecognizer { func isProjectedToDownHalf(maxTranslation: CGFloat) -> Bool { let endLocation = projectedLocation(decelerationRate: .fast) let isPresentationCompleted = endLocation.y > maxTranslation / 2 return isPresentationCompleted } func incrementToBottom(maxTranslation: CGFloat) -> CGFloat { let translation = self.translation(in: view).y setTranslation(.zero, in: nil) let percentIncrement = translation / maxTranslation return percentIncrement } }