SideMenuManager.swift 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229
  1. //
  2. // SideMenuManager.swift
  3. //
  4. // Created by Jon Kent on 12/6/15.
  5. // Copyright © 2015 Jon Kent. All rights reserved.
  6. //
  7. import UIKit
  8. @objcMembers
  9. public class SideMenuManager: NSObject {
  10. final private class SideMenuPanGestureRecognizer: UIPanGestureRecognizer {}
  11. final private class SideMenuScreenEdgeGestureRecognizer: UIScreenEdgePanGestureRecognizer {}
  12. @objc public enum PresentDirection: Int { case
  13. left = 1,
  14. right = 0
  15. init(leftSide: Bool) {
  16. self.init(rawValue: leftSide ? 1 : 0)!
  17. }
  18. var edge: UIRectEdge {
  19. switch self {
  20. case .left: return .left
  21. case .right: return .right
  22. }
  23. }
  24. var name: String {
  25. switch self {
  26. case .left: return "leftMenuNavigationController"
  27. case .right: return "rightMenuNavigationController"
  28. }
  29. }
  30. }
  31. private var _leftMenu: Protected<Menu?> = Protected(nil) { SideMenuManager.setMenu(fromMenu: $0, toMenu: $1) }
  32. private var _rightMenu: Protected<Menu?> = Protected(nil) { SideMenuManager.setMenu(fromMenu: $0, toMenu: $1) }
  33. private var switching: Bool = false
  34. /// Default instance of SideMenuManager.
  35. public static let `default` = SideMenuManager()
  36. /// Default instance of SideMenuManager (objective-C).
  37. public class var defaultManager: SideMenuManager {
  38. return SideMenuManager.default
  39. }
  40. /// The left menu.
  41. open var leftMenuNavigationController: SideMenuNavigationController? {
  42. get {
  43. if _leftMenu.value?.isHidden == true {
  44. _leftMenu.value?.leftSide = true
  45. }
  46. return _leftMenu.value
  47. }
  48. set(menu) { _leftMenu.value = menu }
  49. }
  50. /// The right menu.
  51. open var rightMenuNavigationController: SideMenuNavigationController? {
  52. get {
  53. if _rightMenu.value?.isHidden == true {
  54. _rightMenu.value?.leftSide = false
  55. }
  56. return _rightMenu.value
  57. }
  58. set(menu) { _rightMenu.value = menu }
  59. }
  60. /**
  61. Adds screen edge gestures for both left and right sides to a view to present a menu.
  62. - Parameter toView: The view to add gestures to.
  63. - Returns: The array of screen edge gestures added to `toView`.
  64. */
  65. @discardableResult public func addScreenEdgePanGesturesToPresent(toView view: UIView) -> [UIScreenEdgePanGestureRecognizer] {
  66. return [
  67. addScreenEdgePanGesturesToPresent(toView: view, forMenu: .left),
  68. addScreenEdgePanGesturesToPresent(toView: view, forMenu: .right)
  69. ]
  70. }
  71. /**
  72. Adds screen edge gestures to a view to present a menu.
  73. - Parameter toView: The view to add gestures to.
  74. - Parameter forMenu: The menu (left or right) you want to add a gesture for.
  75. - Returns: The screen edge gestures added to `toView`.
  76. */
  77. @discardableResult public func addScreenEdgePanGesturesToPresent(toView view: UIView, forMenu side: PresentDirection) -> UIScreenEdgePanGestureRecognizer {
  78. if menu(forSide: side) == nil {
  79. let methodName = #function // "addScreenEdgePanGesturesToPresent"
  80. let suggestedMethodName = "addScreenEdgePanGesturesToPresent(toView:forMenu:))"
  81. Print.warning(.screenGestureAdded, arguments: methodName, side.name, suggestedMethodName)
  82. }
  83. return self.addScreenEdgeGesture(to: view, edge: side.edge)
  84. }
  85. /**
  86. Adds a pan edge gesture to a view to present menus.
  87. - Parameter toView: The view to add a pan gesture to.
  88. - Returns: The pan gesture added to `toView`.
  89. */
  90. @discardableResult public func addPanGestureToPresent(toView view: UIView) -> UIPanGestureRecognizer {
  91. if leftMenuNavigationController ?? rightMenuNavigationController == nil {
  92. Print.warning(.panGestureAdded, arguments: #function, PresentDirection.left.name, PresentDirection.right.name, required: true)
  93. }
  94. return addPresentPanGesture(to: view)
  95. }
  96. }
  97. internal extension SideMenuManager {
  98. func setMenu(_ menu: Menu?, forLeftSide leftSide: Bool) {
  99. switch leftSide {
  100. case true: leftMenuNavigationController = menu
  101. case false: rightMenuNavigationController = menu
  102. }
  103. }
  104. private class func setMenu(fromMenu: Menu?, toMenu: Menu?) -> Menu? {
  105. if fromMenu?.isHidden == false {
  106. Print.warning(.menuInUse, arguments: PresentDirection.left.name, required: true)
  107. return fromMenu
  108. }
  109. return toMenu
  110. }
  111. }
  112. private extension SideMenuManager {
  113. @objc func handlePresentMenuScreenEdge(_ gesture: UIScreenEdgePanGestureRecognizer) {
  114. handleMenuPan(gesture)
  115. }
  116. @objc func handlePresentMenuPan(_ gesture: UIPanGestureRecognizer) {
  117. handleMenuPan(gesture)
  118. }
  119. func handleMenuPan(_ gesture: UIPanGestureRecognizer) {
  120. if let activeMenu = activeMenu {
  121. let width = activeMenu.menuWidth
  122. let distance = gesture.xTranslation / width
  123. switch (gesture.state) {
  124. case .changed:
  125. if gesture.canSwitch {
  126. switching = (distance > 0 && !activeMenu.leftSide) || (distance < 0 && activeMenu.leftSide)
  127. if switching {
  128. activeMenu.cancelMenuPan(gesture)
  129. return
  130. }
  131. }
  132. default:
  133. switching = false
  134. }
  135. } else {
  136. let leftSide: Bool
  137. if let gesture = gesture as? UIScreenEdgePanGestureRecognizer {
  138. leftSide = gesture.edges.contains(.left)
  139. } else {
  140. // not sure which way the user is swiping yet, so do nothing
  141. if gesture.xTranslation == 0 { return }
  142. leftSide = gesture.xTranslation > 0
  143. }
  144. guard let menu = menu(forLeftSide: leftSide) else { return }
  145. menu.present(from: topMostViewController, interactively: true)
  146. }
  147. activeMenu?.handleMenuPan(gesture, true)
  148. }
  149. var activeMenu: Menu? {
  150. if leftMenuNavigationController?.isHidden == false { return leftMenuNavigationController }
  151. if rightMenuNavigationController?.isHidden == false { return rightMenuNavigationController }
  152. return nil
  153. }
  154. func menu(forSide: PresentDirection) -> Menu? {
  155. switch forSide {
  156. case .left: return leftMenuNavigationController
  157. case .right: return rightMenuNavigationController
  158. }
  159. }
  160. func menu(forLeftSide leftSide: Bool) -> Menu? {
  161. return menu(forSide: leftSide ? .left : .right)
  162. }
  163. func addScreenEdgeGesture(to view: UIView, edge: UIRectEdge) -> UIScreenEdgePanGestureRecognizer {
  164. if let screenEdgeGestureRecognizer = view.gestureRecognizers?.first(where: { $0 is SideMenuScreenEdgeGestureRecognizer }) as? SideMenuScreenEdgeGestureRecognizer,
  165. screenEdgeGestureRecognizer.edges == edge {
  166. screenEdgeGestureRecognizer.remove()
  167. }
  168. return SideMenuScreenEdgeGestureRecognizer(addTo: view, target: self, action: #selector(handlePresentMenuScreenEdge(_:))).with {
  169. $0.edges = edge
  170. }
  171. }
  172. @discardableResult func addPresentPanGesture(to view: UIView) -> UIPanGestureRecognizer {
  173. if let panGestureRecognizer = view.gestureRecognizers?.first(where: { $0 is SideMenuPanGestureRecognizer }) as? SideMenuPanGestureRecognizer {
  174. return panGestureRecognizer
  175. }
  176. return SideMenuPanGestureRecognizer(addTo: view, target: self, action: #selector(handlePresentMenuPan(_:)))
  177. }
  178. var topMostViewController: UIViewController? {
  179. return UIApplication.shared.keyWindow?.rootViewController?.topMostViewController
  180. }
  181. }
  182. extension SideMenuManager: SideMenuNavigationControllerTransitionDelegate {
  183. internal func sideMenuTransitionDidDismiss(menu: Menu) {
  184. defer { switching = false }
  185. guard switching, let switchToMenu = self.menu(forLeftSide: !menu.leftSide) else { return }
  186. switchToMenu.present(from: topMostViewController, interactively: true)
  187. }
  188. }