SideMenuAnimationController.swift 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232
  1. //
  2. // SideMenuAnimationController.swift
  3. // SideMenu
  4. //
  5. // Created by Jon Kent on 10/24/18.
  6. //
  7. import UIKit
  8. internal protocol AnimationModel {
  9. /// The animation options when a menu is displayed. Ignored when displayed with a gesture.
  10. var animationOptions: UIView.AnimationOptions { get }
  11. /// Duration of the remaining animation when the menu is partially dismissed with gestures. Default is 0.35 seconds.
  12. var completeGestureDuration: Double { get }
  13. /// Duration of the animation when the menu is dismissed without gestures. Default is 0.35 seconds.
  14. var dismissDuration: Double { get }
  15. /// The animation initial spring velocity when a menu is displayed. Ignored when displayed with a gesture.
  16. var initialSpringVelocity: CGFloat { get }
  17. /// Duration of the animation when the menu is presented without gestures. Default is 0.35 seconds.
  18. var presentDuration: Double { get }
  19. /// The animation spring damping when a menu is displayed. Ignored when displayed with a gesture.
  20. var usingSpringWithDamping: CGFloat { get }
  21. }
  22. internal protocol SideMenuAnimationControllerDelegate: class {
  23. func sideMenuAnimationController(_ animationController: SideMenuAnimationController, didDismiss viewController: UIViewController)
  24. func sideMenuAnimationController(_ animationController: SideMenuAnimationController, didPresent viewController: UIViewController)
  25. }
  26. internal final class SideMenuAnimationController: NSObject, UIViewControllerAnimatedTransitioning {
  27. typealias Model = AnimationModel & PresentationModel
  28. private var config: Model
  29. private weak var containerView: UIView?
  30. private let leftSide: Bool
  31. private weak var originalSuperview: UIView?
  32. private var presentationController: SideMenuPresentationController?
  33. private unowned var presentedViewController: UIViewController?
  34. private unowned var presentingViewController: UIViewController?
  35. weak var delegate: SideMenuAnimationControllerDelegate?
  36. init(config: Model, leftSide: Bool, delegate: SideMenuAnimationControllerDelegate? = nil) {
  37. self.config = config
  38. self.leftSide = leftSide
  39. self.delegate = delegate
  40. }
  41. func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
  42. guard
  43. let presentedViewController = transitionContext.presentedViewController,
  44. let presentingViewController = transitionContext.presentingViewController
  45. else { return }
  46. if transitionContext.isPresenting {
  47. self.containerView = transitionContext.containerView
  48. self.presentedViewController = presentedViewController
  49. self.presentingViewController = presentingViewController
  50. self.presentationController = SideMenuPresentationController(
  51. config: config,
  52. leftSide: leftSide,
  53. presentedViewController: presentedViewController,
  54. presentingViewController: presentingViewController,
  55. containerView: transitionContext.containerView
  56. )
  57. }
  58. transition(using: transitionContext)
  59. }
  60. func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
  61. guard let transitionContext = transitionContext else { return 0 }
  62. return duration(presenting: transitionContext.isPresenting, interactive: transitionContext.isInteractive)
  63. }
  64. func animationEnded(_ transitionCompleted: Bool) {
  65. guard let presentedViewController = presentedViewController else { return }
  66. if presentedViewController.isHidden {
  67. delegate?.sideMenuAnimationController(self, didDismiss: presentedViewController)
  68. } else {
  69. delegate?.sideMenuAnimationController(self, didPresent: presentedViewController)
  70. }
  71. }
  72. func transition(presenting: Bool, animated: Bool = true, interactive: Bool = false, alongsideTransition: (() -> Void)? = nil, complete: Bool = true, completion: ((Bool) -> Void)? = nil) {
  73. prepare(presenting: presenting)
  74. transitionWillBegin(presenting: presenting)
  75. transition(
  76. presenting: presenting,
  77. animated: animated,
  78. interactive: interactive,
  79. animations: { [weak self] in
  80. guard let self = self else { return }
  81. self.transition(presenting: presenting)
  82. alongsideTransition?()
  83. }, completion: { [weak self] _ in
  84. guard let self = self else { return }
  85. if complete {
  86. self.transitionDidEnd(presenting: presenting, completed: true)
  87. self.finish(presenting: presenting, completed: true)
  88. }
  89. completion?(true)
  90. })
  91. }
  92. func layout() {
  93. presentationController?.containerViewWillLayoutSubviews()
  94. }
  95. }
  96. private extension SideMenuAnimationController {
  97. func duration(presenting: Bool, interactive: Bool) -> Double {
  98. if interactive { return config.completeGestureDuration }
  99. return presenting ? config.presentDuration : config.dismissDuration
  100. }
  101. func prepare(presenting: Bool) {
  102. guard
  103. presenting,
  104. let presentingViewController = presentingViewController,
  105. let presentedViewController = presentedViewController
  106. else { return }
  107. originalSuperview = presentingViewController.view.superview
  108. containerView?.addSubview(presentingViewController.view)
  109. containerView?.addSubview(presentedViewController.view)
  110. }
  111. func transitionWillBegin(presenting: Bool) {
  112. // prevent any other menu gestures from firing
  113. containerView?.isUserInteractionEnabled = false
  114. if presenting {
  115. presentationController?.presentationTransitionWillBegin()
  116. } else {
  117. presentationController?.dismissalTransitionWillBegin()
  118. }
  119. }
  120. func transition(presenting: Bool) {
  121. if presenting {
  122. presentationController?.presentationTransition()
  123. } else {
  124. presentationController?.dismissalTransition()
  125. }
  126. }
  127. func transitionDidEnd(presenting: Bool, completed: Bool) {
  128. if presenting {
  129. presentationController?.presentationTransitionDidEnd(completed)
  130. } else {
  131. presentationController?.dismissalTransitionDidEnd(completed)
  132. }
  133. containerView?.isUserInteractionEnabled = true
  134. }
  135. func finish(presenting: Bool, completed: Bool) {
  136. guard
  137. presenting != completed,
  138. let presentingViewController = self.presentingViewController
  139. else { return }
  140. presentedViewController?.view.removeFromSuperview()
  141. originalSuperview?.addSubview(presentingViewController.view)
  142. }
  143. func transition(using transitionContext: UIViewControllerContextTransitioning) {
  144. prepare(presenting: transitionContext.isPresenting)
  145. transitionWillBegin(presenting: transitionContext.isPresenting)
  146. transition(
  147. presenting: transitionContext.isPresenting,
  148. animated: transitionContext.isAnimated,
  149. interactive: transitionContext.isInteractive,
  150. animations: { [weak self] in
  151. guard let self = self else { return }
  152. self.transition(presenting: transitionContext.isPresenting)
  153. }, completion: { [weak self] _ in
  154. guard let self = self else { return }
  155. let completed = !transitionContext.transitionWasCancelled
  156. self.transitionDidEnd(presenting: transitionContext.isPresenting, completed: completed)
  157. self.finish(presenting: transitionContext.isPresenting, completed: completed)
  158. // Called last. This causes the transition container to be removed and animationEnded() to be called.
  159. transitionContext.completeTransition(completed)
  160. })
  161. }
  162. func transition(presenting: Bool, animated: Bool = true, interactive: Bool = false, animations: @escaping (() -> Void) = {}, completion: @escaping ((Bool) -> Void) = { _ in }) {
  163. if !animated {
  164. animations()
  165. completion(true)
  166. return
  167. }
  168. let duration = self.duration(presenting: presenting, interactive: interactive)
  169. if interactive {
  170. // IMPORTANT: The non-interactive animation block will not complete if adapted for interactive. The below animation block must be used!
  171. UIView.animate(
  172. withDuration: duration,
  173. delay: duration, // HACK: If zero, the animation briefly flashes in iOS 11.
  174. options: .curveLinear,
  175. animations: animations,
  176. completion: completion
  177. )
  178. return
  179. }
  180. UIView.animate(
  181. withDuration: duration,
  182. delay: 0,
  183. usingSpringWithDamping: config.usingSpringWithDamping,
  184. initialSpringVelocity: config.initialSpringVelocity,
  185. options: config.animationOptions,
  186. animations: animations,
  187. completion: completion
  188. )
  189. }
  190. }
  191. private extension UIViewControllerContextTransitioning {
  192. var isPresenting: Bool {
  193. return viewController(forKey: .from)?.presentedViewController === viewController(forKey: .to)
  194. }
  195. var presentingViewController: UIViewController? {
  196. return viewController(forKey: isPresenting ? .from : .to)
  197. }
  198. var presentedViewController: UIViewController? {
  199. return viewController(forKey: isPresenting ? .to : .from)
  200. }
  201. }