SideMenuPresentationController.swift 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316
  1. //
  2. // BasePresentationController.swift
  3. // SideMenu
  4. //
  5. // Created by Jon Kent on 10/20/18.
  6. //
  7. import UIKit
  8. internal protocol PresentationModel {
  9. /// Draws `presentStyle.backgroundColor` behind the status bar. Default is 1.
  10. var statusBarEndAlpha: CGFloat { get }
  11. /// Enable or disable interaction with the presenting view controller while the menu is displayed. Enabling may make it difficult to dismiss the menu or cause exceptions if the user tries to present and already presented menu. `presentingViewControllerUseSnapshot` must also set to false. Default is false.
  12. var presentingViewControllerUserInteractionEnabled: Bool { get }
  13. /// Use a snapshot for the presenting vierw controller while the menu is displayed. Useful when layout changes occur during transitions. Not recommended for apps that support rotation. Default is false.
  14. var presentingViewControllerUseSnapshot: Bool { get }
  15. /// The presentation style of the menu.
  16. var presentationStyle: SideMenuPresentationStyle { get }
  17. /// Width of the menu when presented on screen, showing the existing view controller in the remaining space. Default is zero.
  18. var menuWidth: CGFloat { get }
  19. }
  20. internal protocol SideMenuPresentationControllerDelegate: class {
  21. func sideMenuPresentationControllerDidTap(_ presentationController: SideMenuPresentationController)
  22. func sideMenuPresentationController(_ presentationController: SideMenuPresentationController, didPanWith gesture: UIPanGestureRecognizer)
  23. }
  24. internal final class SideMenuPresentationController {
  25. private let config: PresentationModel
  26. private weak var containerView: UIView?
  27. private var interactivePopGestureRecognizerEnabled: Bool?
  28. private var clipsToBounds: Bool?
  29. private let leftSide: Bool
  30. private weak var presentedViewController: UIViewController?
  31. private weak var presentingViewController: UIViewController?
  32. private lazy var snapshotView: UIView? = {
  33. guard config.presentingViewControllerUseSnapshot,
  34. let view = presentingViewController?.view.snapshotView(afterScreenUpdates: true) else {
  35. return nil
  36. }
  37. view.autoresizingMask = [.flexibleHeight, .flexibleWidth]
  38. return view
  39. }()
  40. private lazy var statusBarView: UIView? = {
  41. guard config.statusBarEndAlpha > .leastNonzeroMagnitude else { return nil }
  42. return UIView {
  43. $0.backgroundColor = config.presentationStyle.backgroundColor
  44. $0.autoresizingMask = [.flexibleHeight, .flexibleWidth]
  45. $0.isUserInteractionEnabled = false
  46. }
  47. }()
  48. required init(config: PresentationModel, leftSide: Bool, presentedViewController: UIViewController, presentingViewController: UIViewController, containerView: UIView) {
  49. self.config = config
  50. self.containerView = containerView
  51. self.leftSide = leftSide
  52. self.presentedViewController = presentedViewController
  53. self.presentingViewController = presentingViewController
  54. }
  55. deinit {
  56. guard presentedViewController?.isHidden == false else { return }
  57. // Presentations must be reversed to preserve user experience
  58. dismissalTransitionWillBegin()
  59. dismissalTransition()
  60. dismissalTransitionDidEnd(true)
  61. }
  62. func containerViewWillLayoutSubviews() {
  63. guard let containerView = containerView,
  64. let presentedViewController = presentedViewController,
  65. let presentingViewController = presentingViewController
  66. else { return }
  67. presentedViewController.view.untransform {
  68. presentedViewController.view.frame = frameOfPresentedViewInContainerView
  69. }
  70. presentingViewController.view.untransform {
  71. presentingViewController.view.frame = frameOfPresentingViewInContainerView
  72. snapshotView?.frame = presentingViewController.view.bounds
  73. }
  74. guard let statusBarView = statusBarView else { return }
  75. var statusBarFrame: CGRect = self.statusBarFrame
  76. statusBarFrame.size.height -= containerView.frame.minY
  77. statusBarView.frame = statusBarFrame
  78. }
  79. func presentationTransitionWillBegin() {
  80. guard let containerView = containerView,
  81. let presentedViewController = presentedViewController,
  82. let presentingViewController = presentingViewController
  83. else { return }
  84. if let snapshotView = snapshotView {
  85. presentingViewController.view.addSubview(snapshotView)
  86. }
  87. presentingViewController.view.isUserInteractionEnabled = config.presentingViewControllerUserInteractionEnabled
  88. containerView.backgroundColor = config.presentationStyle.backgroundColor
  89. layerViews()
  90. if let statusBarView = statusBarView {
  91. containerView.addSubview(statusBarView)
  92. }
  93. dismissalTransition()
  94. config.presentationStyle.presentationTransitionWillBegin(to: presentedViewController, from: presentingViewController)
  95. }
  96. func presentationTransition() {
  97. guard let presentedViewController = presentedViewController,
  98. let presentingViewController = presentingViewController
  99. else { return }
  100. transition(
  101. to: presentedViewController,
  102. from: presentingViewController,
  103. alpha: config.presentationStyle.presentingEndAlpha,
  104. statusBarAlpha: config.statusBarEndAlpha,
  105. scale: config.presentationStyle.presentingScaleFactor,
  106. translate: config.presentationStyle.presentingTranslateFactor
  107. )
  108. config.presentationStyle.presentationTransition(to: presentedViewController, from: presentingViewController)
  109. }
  110. func presentationTransitionDidEnd(_ completed: Bool) {
  111. guard completed else {
  112. snapshotView?.removeFromSuperview()
  113. dismissalTransitionDidEnd(!completed)
  114. return
  115. }
  116. guard let presentedViewController = presentedViewController,
  117. let presentingViewController = presentingViewController
  118. else { return }
  119. addParallax(to: presentingViewController.view)
  120. if let topNavigationController = presentingViewController as? UINavigationController {
  121. interactivePopGestureRecognizerEnabled = interactivePopGestureRecognizerEnabled ?? topNavigationController.interactivePopGestureRecognizer?.isEnabled
  122. topNavigationController.interactivePopGestureRecognizer?.isEnabled = false
  123. }
  124. containerViewWillLayoutSubviews()
  125. config.presentationStyle.presentationTransitionDidEnd(to: presentedViewController, from: presentingViewController, completed)
  126. }
  127. func dismissalTransitionWillBegin() {
  128. snapshotView?.removeFromSuperview()
  129. presentationTransition()
  130. guard let presentedViewController = presentedViewController,
  131. let presentingViewController = presentingViewController
  132. else { return }
  133. config.presentationStyle.dismissalTransitionWillBegin(to: presentedViewController, from: presentingViewController)
  134. }
  135. func dismissalTransition() {
  136. guard let presentedViewController = presentedViewController,
  137. let presentingViewController = presentingViewController
  138. else { return }
  139. transition(
  140. to: presentingViewController,
  141. from: presentedViewController,
  142. alpha: config.presentationStyle.menuStartAlpha,
  143. statusBarAlpha: 0,
  144. scale: config.presentationStyle.menuScaleFactor,
  145. translate: config.presentationStyle.menuTranslateFactor
  146. )
  147. config.presentationStyle.dismissalTransition(to: presentedViewController, from: presentingViewController)
  148. }
  149. func dismissalTransitionDidEnd(_ completed: Bool) {
  150. guard completed else {
  151. if let snapshotView = snapshotView, let presentingViewController = presentingViewController {
  152. presentingViewController.view.addSubview(snapshotView)
  153. }
  154. presentationTransitionDidEnd(!completed)
  155. return
  156. }
  157. guard let presentedViewController = presentedViewController,
  158. let presentingViewController = presentingViewController
  159. else { return }
  160. statusBarView?.removeFromSuperview()
  161. removeStyles(from: presentingViewController.containerViewController.view)
  162. if let interactivePopGestureRecognizerEnabled = interactivePopGestureRecognizerEnabled,
  163. let topNavigationController = presentingViewController as? UINavigationController {
  164. topNavigationController.interactivePopGestureRecognizer?.isEnabled = interactivePopGestureRecognizerEnabled
  165. }
  166. presentingViewController.view.isUserInteractionEnabled = true
  167. config.presentationStyle.dismissalTransitionDidEnd(to: presentedViewController, from: presentingViewController, completed)
  168. }
  169. }
  170. private extension SideMenuPresentationController {
  171. var statusBarFrame: CGRect {
  172. if #available(iOS 13.0, *) {
  173. return containerView?.window?.windowScene?.statusBarManager?.statusBarFrame ?? .zero
  174. } else {
  175. return UIApplication.shared.statusBarFrame
  176. }
  177. }
  178. var frameOfPresentedViewInContainerView: CGRect {
  179. guard let containerView = containerView else { return .zero }
  180. var rect = containerView.bounds
  181. rect.origin.x = leftSide ? 0 : rect.width - config.menuWidth
  182. rect.size.width = config.menuWidth
  183. return rect
  184. }
  185. var frameOfPresentingViewInContainerView: CGRect {
  186. guard let containerView = containerView else { return .zero }
  187. var rect = containerView.frame
  188. if containerView.superview != nil, containerView.frame.minY > .ulpOfOne {
  189. let statusBarOffset = statusBarFrame.height - rect.minY
  190. rect.origin.y = statusBarOffset
  191. rect.size.height -= statusBarOffset
  192. }
  193. return rect
  194. }
  195. func transition(to: UIViewController, from: UIViewController, alpha: CGFloat, statusBarAlpha: CGFloat, scale: CGFloat, translate: CGFloat) {
  196. containerViewWillLayoutSubviews()
  197. to.view.transform = .identity
  198. to.view.alpha = 1
  199. let x = (leftSide ? 1 : -1) * config.menuWidth * translate
  200. from.view.alpha = alpha
  201. from.view.transform = CGAffineTransform
  202. .identity
  203. .translatedBy(x: x, y: 0)
  204. .scaledBy(x: scale, y: scale)
  205. statusBarView?.alpha = statusBarAlpha
  206. }
  207. func layerViews() {
  208. guard let presentedViewController = presentedViewController,
  209. let presentingViewController = presentingViewController
  210. else { return }
  211. statusBarView?.layer.zPosition = 2
  212. if config.presentationStyle.menuOnTop {
  213. addShadow(to: presentedViewController.view)
  214. presentedViewController.view.layer.zPosition = 1
  215. } else {
  216. addShadow(to: presentingViewController.view)
  217. presentedViewController.view.layer.zPosition = -1
  218. }
  219. }
  220. func addShadow(to view: UIView) {
  221. view.layer.shadowColor = config.presentationStyle.onTopShadowColor.cgColor
  222. view.layer.shadowRadius = config.presentationStyle.onTopShadowRadius
  223. view.layer.shadowOpacity = config.presentationStyle.onTopShadowOpacity
  224. view.layer.shadowOffset = config.presentationStyle.onTopShadowOffset
  225. clipsToBounds = clipsToBounds ?? view.clipsToBounds
  226. view.clipsToBounds = false
  227. }
  228. func addParallax(to view: UIView) {
  229. var effects: [UIInterpolatingMotionEffect] = []
  230. let x = config.presentationStyle.presentingParallaxStrength.width
  231. if x > 0 {
  232. let horizontal = UIInterpolatingMotionEffect(keyPath: "center.x", type: .tiltAlongHorizontalAxis)
  233. horizontal.minimumRelativeValue = -x
  234. horizontal.maximumRelativeValue = x
  235. effects.append(horizontal)
  236. }
  237. let y = config.presentationStyle.presentingParallaxStrength.height
  238. if y > 0 {
  239. let vertical = UIInterpolatingMotionEffect(keyPath: "center.y", type: .tiltAlongVerticalAxis)
  240. vertical.minimumRelativeValue = -y
  241. vertical.maximumRelativeValue = y
  242. effects.append(vertical)
  243. }
  244. if effects.count > 0 {
  245. let group = UIMotionEffectGroup()
  246. group.motionEffects = effects
  247. view.motionEffects.removeAll()
  248. view.addMotionEffect(group)
  249. }
  250. }
  251. func removeStyles(from view: UIView) {
  252. view.motionEffects.removeAll()
  253. view.layer.shadowOpacity = 0
  254. view.layer.shadowOpacity = 0
  255. view.clipsToBounds = clipsToBounds ?? true
  256. clipsToBounds = false
  257. }
  258. }