StandardInteractionController.swift 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215
  1. // MIT License
  2. //
  3. // Copyright (c) 2021 Daniel Gauthier
  4. //
  5. // Permission is hereby granted, free of charge, to any person obtaining a copy
  6. // of this software and associated documentation files (the "Software"), to deal
  7. // in the Software without restriction, including without limitation the rights
  8. // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  9. // copies of the Software, and to permit persons to whom the Software is
  10. // furnished to do so, subject to the following conditions:
  11. //
  12. // The above copyright notice and this permission notice shall be included in all
  13. // copies or substantial portions of the Software.
  14. //
  15. // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  16. // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  17. // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  18. // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  19. // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  20. // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  21. // SOFTWARE.
  22. //
  23. // From https://github.com/danielmgauthier/ViewControllerTransitionExample
  24. // SPDX-License-Identifier: MIT
  25. import UIKit
  26. class StandardInteractionController: NSObject, InteractionControlling {
  27. var interactionInProgress = false
  28. private weak var viewController: CustomPresentable!
  29. private weak var transitionContext: UIViewControllerContextTransitioning?
  30. private var interactionDistance: CGFloat = 0
  31. private var interruptedTranslation: CGFloat = 0
  32. private var presentedFrame: CGRect?
  33. private var cancellationAnimator: UIViewPropertyAnimator?
  34. // MARK: - Setup
  35. init(viewController: CustomPresentable) {
  36. self.viewController = viewController
  37. super.init()
  38. prepareGestureRecognizer(in: viewController.view)
  39. if let scrollView = viewController.dismissalHandlingScrollView {
  40. resolveScrollViewGestures(scrollView)
  41. }
  42. // Round corners only at the top for the presented viewController
  43. self.viewController.view.clipsToBounds = true
  44. self.viewController.view.layer.cornerRadius = 10
  45. self.viewController.view.layer.maskedCorners = [.layerMaxXMinYCorner, .layerMinXMinYCorner]
  46. }
  47. private func prepareGestureRecognizer(in view: UIView) {
  48. let gesture = OneWayPanGestureRecognizer(target: self, action: #selector(handleGesture(_:)))
  49. gesture.delegate = self
  50. view.addGestureRecognizer(gesture)
  51. }
  52. private func resolveScrollViewGestures(_ scrollView: UIScrollView) {
  53. let scrollGestureRecognizer = OneWayPanGestureRecognizer(target: self, action: #selector(handleGesture(_:)))
  54. scrollGestureRecognizer.delegate = self
  55. scrollView.addGestureRecognizer(scrollGestureRecognizer)
  56. scrollView.panGestureRecognizer.require(toFail: scrollGestureRecognizer)
  57. }
  58. // MARK: - Gesture handling
  59. @objc func handleGesture(_ gestureRecognizer: OneWayPanGestureRecognizer) {
  60. guard let superview = gestureRecognizer.view?.superview else { return }
  61. let translation = gestureRecognizer.translation(in: superview).y
  62. let velocity = gestureRecognizer.velocity(in: superview).y
  63. switch gestureRecognizer.state {
  64. case .began: gestureBegan()
  65. case .changed: gestureChanged(translation: translation + interruptedTranslation, velocity: velocity)
  66. case .cancelled: gestureCancelled(translation: translation + interruptedTranslation, velocity: velocity)
  67. case .ended: gestureEnded(translation: translation + interruptedTranslation, velocity: velocity)
  68. default: break
  69. }
  70. }
  71. private func gestureBegan() {
  72. disableOtherTouches()
  73. cancellationAnimator?.stopAnimation(true)
  74. if let presentedFrame = presentedFrame {
  75. interruptedTranslation = viewController.view.frame.minY - presentedFrame.minY
  76. }
  77. if !interactionInProgress {
  78. interactionInProgress = true
  79. viewController.dismiss(animated: true)
  80. }
  81. }
  82. private func gestureChanged(translation: CGFloat, velocity: CGFloat) {
  83. var progress = interactionDistance == 0 ? 0 : (translation / interactionDistance)
  84. if progress < 0 { progress /= (1.0 + abs(progress * 20)) }
  85. update(progress: progress)
  86. }
  87. private func gestureCancelled(translation: CGFloat, velocity: CGFloat) {
  88. cancel(initialSpringVelocity: springVelocity(distanceToTravel: -translation, gestureVelocity: velocity))
  89. }
  90. private func gestureEnded(translation: CGFloat, velocity: CGFloat) {
  91. if velocity > 300 || (translation > interactionDistance / 2.0 && velocity > -300) {
  92. finish(initialSpringVelocity: springVelocity(distanceToTravel: interactionDistance - translation, gestureVelocity: velocity))
  93. } else {
  94. cancel(initialSpringVelocity: springVelocity(distanceToTravel: -translation, gestureVelocity: velocity))
  95. }
  96. }
  97. // MARK: - Transition controlling
  98. func startInteractiveTransition(_ transitionContext: UIViewControllerContextTransitioning) {
  99. let presentedViewController = transitionContext.viewController(forKey: .from)!
  100. presentedFrame = transitionContext.finalFrame(for: presentedViewController)
  101. self.transitionContext = transitionContext
  102. interactionDistance = transitionContext.containerView.bounds.height - presentedFrame!.minY
  103. }
  104. func update(progress: CGFloat) {
  105. guard let transitionContext = transitionContext, let presentedFrame = presentedFrame else { return }
  106. transitionContext.updateInteractiveTransition(progress)
  107. let presentedViewController = transitionContext.viewController(forKey: .from)!
  108. presentedViewController.view.frame = CGRect(x: presentedFrame.minX, y: presentedFrame.minY + interactionDistance * progress, width: presentedFrame.width, height: presentedFrame.height)
  109. if let modalPresentationController = presentedViewController.presentationController as? ModalPresentationController {
  110. modalPresentationController.fadeView.alpha = 1.0 - progress
  111. }
  112. }
  113. func cancel(initialSpringVelocity: CGFloat) {
  114. guard let transitionContext = transitionContext, let presentedFrame = presentedFrame else { return }
  115. let presentedViewController = transitionContext.viewController(forKey: .from)!
  116. let timingParameters = UISpringTimingParameters(dampingRatio: 0.8, initialVelocity: CGVector(dx: 0, dy: initialSpringVelocity))
  117. cancellationAnimator = UIViewPropertyAnimator(duration: 0.5, timingParameters: timingParameters)
  118. cancellationAnimator?.addAnimations {
  119. presentedViewController.view.frame = presentedFrame
  120. if let modalPresentationController = presentedViewController.presentationController as? ModalPresentationController {
  121. modalPresentationController.fadeView.alpha = 1.0
  122. }
  123. }
  124. cancellationAnimator?.addCompletion { _ in
  125. transitionContext.cancelInteractiveTransition()
  126. transitionContext.completeTransition(false)
  127. self.interactionInProgress = false
  128. self.enableOtherTouches()
  129. }
  130. cancellationAnimator?.startAnimation()
  131. }
  132. func finish(initialSpringVelocity: CGFloat) {
  133. guard let transitionContext = transitionContext,
  134. let presentedFrame = presentedFrame,
  135. let presentedViewController = transitionContext.viewController(forKey: .from) as? CustomPresentable
  136. else { return }
  137. let dismissedFrame = CGRect(x: presentedFrame.minX, y: transitionContext.containerView.bounds.height, width: presentedFrame.width, height: presentedFrame.height)
  138. let timingParameters = UISpringTimingParameters(dampingRatio: 0.8, initialVelocity: CGVector(dx: 0, dy: initialSpringVelocity))
  139. let finishAnimator = UIViewPropertyAnimator(duration: 0.5, timingParameters: timingParameters)
  140. finishAnimator.addAnimations {
  141. presentedViewController.view.frame = dismissedFrame
  142. if let modalPresentationController = presentedViewController.presentationController as? ModalPresentationController {
  143. modalPresentationController.fadeView.alpha = 0.0
  144. }
  145. }
  146. finishAnimator.addCompletion { _ in
  147. transitionContext.finishInteractiveTransition()
  148. transitionContext.completeTransition(true)
  149. self.interactionInProgress = false
  150. }
  151. finishAnimator.startAnimation()
  152. }
  153. // MARK: - Helpers
  154. private func springVelocity(distanceToTravel: CGFloat, gestureVelocity: CGFloat) -> CGFloat {
  155. distanceToTravel == 0 ? 0 : gestureVelocity / distanceToTravel
  156. }
  157. private func disableOtherTouches() {
  158. viewController.view.subviews.forEach {
  159. $0.isUserInteractionEnabled = false
  160. }
  161. }
  162. private func enableOtherTouches() {
  163. viewController.view.subviews.forEach {
  164. $0.isUserInteractionEnabled = true
  165. }
  166. }
  167. }
  168. // MARK: - UIGestureRecognizerDelegate
  169. extension StandardInteractionController: UIGestureRecognizerDelegate {
  170. func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
  171. if let scrollView = viewController.dismissalHandlingScrollView {
  172. return scrollView.contentOffset.y <= 0
  173. }
  174. return viewController.dismissalGestureEnabled
  175. }
  176. }