SideMenuNavigationController.swift 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656
  1. //
  2. // SideMenuNavigationController.swift
  3. //
  4. // Created by Jon Kent on 1/14/16.
  5. // Copyright © 2016 Jon Kent. All rights reserved.
  6. //
  7. import UIKit
  8. @objc public enum SideMenuPushStyle: Int { case
  9. `default`,
  10. popWhenPossible,
  11. preserve,
  12. preserveAndHideBackButton,
  13. replace,
  14. subMenu
  15. internal var hidesBackButton: Bool {
  16. switch self {
  17. case .preserveAndHideBackButton, .replace: return true
  18. case .default, .popWhenPossible, .preserve, .subMenu: return false
  19. }
  20. }
  21. }
  22. internal protocol MenuModel {
  23. /// Prevents the same view controller (or a view controller of the same class) from being pushed more than once. Defaults to true.
  24. var allowPushOfSameClassTwice: Bool { get }
  25. /// Forces menus to always animate when appearing or disappearing, regardless of a pushed view controller's animation.
  26. var alwaysAnimate: Bool { get }
  27. /**
  28. The blur effect style of the menu if the menu's root view controller is a UITableViewController or UICollectionViewController.
  29. - Note: If you want cells in a UITableViewController menu to show vibrancy, make them a subclass of UITableViewVibrantCell.
  30. */
  31. var blurEffectStyle: UIBlurEffect.Style? { get }
  32. /// Animation curve of the remaining animation when the menu is partially dismissed with gestures. Default is .easeIn.
  33. var completionCurve: UIView.AnimationCurve { get }
  34. /// Automatically dismisses the menu when another view is presented from it.
  35. var dismissOnPresent: Bool { get }
  36. /// Automatically dismisses the menu when another view controller is pushed from it.
  37. var dismissOnPush: Bool { get }
  38. /// Automatically dismisses the menu when the screen is rotated.
  39. var dismissOnRotation: Bool { get }
  40. /// Automatically dismisses the menu when app goes to the background.
  41. var dismissWhenBackgrounded: Bool { get }
  42. /// Enable or disable a swipe gesture that dismisses the menu. Will not be triggered when `presentingViewControllerUserInteractionEnabled` is set to true. Default is true.
  43. var enableSwipeToDismissGesture: Bool { get }
  44. /// Enable or disable a tap gesture that dismisses the menu. Will not be triggered when `presentingViewControllerUserInteractionEnabled` is set to true. Default is true.
  45. var enableTapToDismissGesture: Bool { get }
  46. /**
  47. The push style of the menu.
  48. There are six modes in MenuPushStyle:
  49. - defaultBehavior: The view controller is pushed onto the stack.
  50. - popWhenPossible: If a view controller already in the stack is of the same class as the pushed view controller, the stack is instead popped back to the existing view controller. This behavior can help users from getting lost in a deep navigation stack.
  51. - preserve: If a view controller already in the stack is of the same class as the pushed view controller, the existing view controller is pushed to the end of the stack. This behavior is similar to a UITabBarController.
  52. - preserveAndHideBackButton: Same as .preserve and back buttons are automatically hidden.
  53. - replace: Any existing view controllers are released from the stack and replaced with the pushed view controller. Back buttons are automatically hidden. This behavior is ideal if view controllers require a lot of memory or their state doesn't need to be preserved..
  54. - subMenu: Unlike all other behaviors that push using the menu's presentingViewController, this behavior pushes view controllers within the menu. Use this behavior if you want to display a sub menu.
  55. */
  56. var pushStyle: SideMenuPushStyle { get }
  57. }
  58. @objc public protocol SideMenuNavigationControllerDelegate {
  59. @objc optional func sideMenuWillAppear(menu: SideMenuNavigationController, animated: Bool)
  60. @objc optional func sideMenuDidAppear(menu: SideMenuNavigationController, animated: Bool)
  61. @objc optional func sideMenuWillDisappear(menu: SideMenuNavigationController, animated: Bool)
  62. @objc optional func sideMenuDidDisappear(menu: SideMenuNavigationController, animated: Bool)
  63. }
  64. internal protocol SideMenuNavigationControllerTransitionDelegate: class {
  65. func sideMenuTransitionDidDismiss(menu: Menu)
  66. }
  67. public struct SideMenuSettings: Model, InitializableStruct {
  68. public var allowPushOfSameClassTwice: Bool = true
  69. public var alwaysAnimate: Bool = true
  70. public var animationOptions: UIView.AnimationOptions = .curveEaseInOut
  71. public var blurEffectStyle: UIBlurEffect.Style? = nil
  72. public var completeGestureDuration: Double = 0.35
  73. public var completionCurve: UIView.AnimationCurve = .easeIn
  74. public var dismissDuration: Double = 0.35
  75. public var dismissOnPresent: Bool = true
  76. public var dismissOnPush: Bool = true
  77. public var dismissOnRotation: Bool = true
  78. public var dismissWhenBackgrounded: Bool = true
  79. public var enableSwipeToDismissGesture: Bool = true
  80. public var enableTapToDismissGesture: Bool = true
  81. public var initialSpringVelocity: CGFloat = 1
  82. public var menuWidth: CGFloat = {
  83. let appScreenRect = UIApplication.shared.keyWindow?.bounds ?? UIWindow().bounds
  84. let minimumSize = min(appScreenRect.width, appScreenRect.height)
  85. return min(round(minimumSize * 0.75), 240)
  86. }()
  87. public var presentingViewControllerUserInteractionEnabled: Bool = false
  88. public var presentingViewControllerUseSnapshot: Bool = false
  89. public var presentDuration: Double = 0.35
  90. public var presentationStyle: SideMenuPresentationStyle = .viewSlideOut
  91. public var pushStyle: SideMenuPushStyle = .default
  92. public var statusBarEndAlpha: CGFloat = 0
  93. public var usingSpringWithDamping: CGFloat = 1
  94. public init() {}
  95. }
  96. internal typealias Menu = SideMenuNavigationController
  97. typealias Model = MenuModel & PresentationModel & AnimationModel
  98. @objcMembers
  99. open class SideMenuNavigationController: UINavigationController {
  100. private lazy var _leftSide = Protected(false) { [weak self] oldValue, newValue in
  101. guard self?.isHidden != false else {
  102. Print.warning(.property, arguments: .leftSide, required: true)
  103. return oldValue
  104. }
  105. return newValue
  106. }
  107. private weak var _sideMenuManager: SideMenuManager?
  108. private weak var foundViewController: UIViewController?
  109. private var originalBackgroundColor: UIColor?
  110. private var rotating: Bool = false
  111. private var transitionController: SideMenuTransitionController?
  112. private var transitionInteractive: Bool = false
  113. /// Delegate for receiving appear and disappear related events. If `nil` the visible view controller that displays a `SideMenuNavigationController` automatically receives these events.
  114. public weak var sideMenuDelegate: SideMenuNavigationControllerDelegate?
  115. /// The swipe to dismiss gesture.
  116. open private(set) weak var swipeToDismissGesture: UIPanGestureRecognizer? = nil
  117. /// The tap to dismiss gesture.
  118. open private(set) weak var tapToDismissGesture: UITapGestureRecognizer? = nil
  119. open var sideMenuManager: SideMenuManager {
  120. get { return _sideMenuManager ?? SideMenuManager.default }
  121. set {
  122. newValue.setMenu(self, forLeftSide: leftSide)
  123. if let sideMenuManager = _sideMenuManager, sideMenuManager !== newValue {
  124. let side = SideMenuManager.PresentDirection(leftSide: leftSide)
  125. Print.warning(.menuAlreadyAssigned, arguments: String(describing: self.self), side.name, String(describing: newValue))
  126. }
  127. _sideMenuManager = newValue
  128. }
  129. }
  130. /// The menu settings.
  131. open var settings = SideMenuSettings() {
  132. didSet {
  133. setupBlur()
  134. if !enableSwipeToDismissGesture {
  135. swipeToDismissGesture?.remove()
  136. }
  137. if !enableTapToDismissGesture {
  138. tapToDismissGesture?.remove()
  139. }
  140. }
  141. }
  142. public override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) {
  143. super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
  144. setup()
  145. }
  146. public init(rootViewController: UIViewController, settings: SideMenuSettings = SideMenuSettings()) {
  147. self.settings = settings
  148. super.init(rootViewController: rootViewController)
  149. setup()
  150. }
  151. required public init?(coder aDecoder: NSCoder) {
  152. super.init(coder: aDecoder)
  153. setup()
  154. }
  155. override open func awakeFromNib() {
  156. super.awakeFromNib()
  157. sideMenuManager.setMenu(self, forLeftSide: leftSide)
  158. }
  159. override open func viewWillAppear(_ animated: Bool) {
  160. super.viewWillAppear(animated)
  161. if topViewController == nil {
  162. Print.warning(.emptyMenu)
  163. }
  164. // Dismiss keyboard to prevent weird keyboard animations from occurring during transition
  165. presentingViewController?.view.endEditing(true)
  166. foundViewController = nil
  167. activeDelegate?.sideMenuWillAppear?(menu: self, animated: animated)
  168. }
  169. override open func viewDidAppear(_ animated: Bool) {
  170. super.viewDidAppear(animated)
  171. // We had presented a view before, so lets dismiss ourselves as already acted upon
  172. if view.isHidden {
  173. dismiss(animated: false, completion: { [weak self] in
  174. self?.view.isHidden = false
  175. })
  176. } else {
  177. activeDelegate?.sideMenuDidAppear?(menu: self, animated: animated)
  178. }
  179. }
  180. override open func viewWillDisappear(_ animated: Bool) {
  181. super.viewWillDisappear(animated)
  182. defer { activeDelegate?.sideMenuWillDisappear?(menu: self, animated: animated) }
  183. guard !isBeingDismissed else { return }
  184. // When presenting a view controller from the menu, the menu view gets moved into another transition view above our transition container
  185. // which can break the visual layout we had before. So, we move the menu view back to its original transition view to preserve it.
  186. if let presentingView = presentingViewController?.view, let containerView = presentingView.superview {
  187. containerView.addSubview(view)
  188. }
  189. if dismissOnPresent {
  190. // We're presenting a view controller from the menu, so we need to hide the menu so it isn't showing when the presented view is dismissed.
  191. transitionController?.transition(presenting: false, animated: animated, alongsideTransition: { [weak self] in
  192. guard let self = self else { return }
  193. self.activeDelegate?.sideMenuWillDisappear?(menu: self, animated: animated)
  194. }, complete: false, completion: { [weak self] _ in
  195. guard let self = self else { return }
  196. self.activeDelegate?.sideMenuDidDisappear?(menu: self, animated: animated)
  197. self.view.isHidden = true
  198. })
  199. }
  200. }
  201. override open func viewDidDisappear(_ animated: Bool) {
  202. super.viewDidDisappear(animated)
  203. // Work-around: if the menu is dismissed without animation the transition logic is never called to restore the
  204. // the view hierarchy leaving the screen black/empty. This is because the transition moves views within a container
  205. // view, but dismissing without animation removes the container view before the original hierarchy is restored.
  206. // This check corrects that.
  207. if isBeingDismissed {
  208. transitionController?.transition(presenting: false, animated: false)
  209. }
  210. // Clear selection on UITableViewControllers when reappearing using custom transitions
  211. if let tableViewController = topViewController as? UITableViewController,
  212. let tableView = tableViewController.tableView,
  213. let indexPaths = tableView.indexPathsForSelectedRows,
  214. tableViewController.clearsSelectionOnViewWillAppear {
  215. indexPaths.forEach { tableView.deselectRow(at: $0, animated: false) }
  216. }
  217. activeDelegate?.sideMenuDidDisappear?(menu: self, animated: animated)
  218. if isBeingDismissed {
  219. transitionController = nil
  220. } else if dismissOnPresent {
  221. view.isHidden = true
  222. }
  223. }
  224. override open func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
  225. super.viewWillTransition(to: size, with: coordinator)
  226. // Don't bother resizing if the view isn't visible
  227. guard let transitionController = transitionController, !view.isHidden else { return }
  228. rotating = true
  229. let dismiss = self.presentingViewControllerUseSnapshot || self.dismissOnRotation
  230. coordinator.animate(alongsideTransition: { _ in
  231. if dismiss {
  232. transitionController.transition(presenting: false, animated: false, complete: false)
  233. } else {
  234. transitionController.layout()
  235. }
  236. }) { [weak self] _ in
  237. guard let self = self else { return }
  238. if dismiss {
  239. self.dismissMenu(animated: false)
  240. }
  241. self.rotating = false
  242. }
  243. }
  244. open override func viewWillLayoutSubviews() {
  245. super.viewWillLayoutSubviews()
  246. transitionController?.layout()
  247. }
  248. override open func pushViewController(_ viewController: UIViewController, animated: Bool) {
  249. guard viewControllers.count > 0 else {
  250. // NOTE: pushViewController is called by init(rootViewController: UIViewController)
  251. // so we must perform the normal super method in this case
  252. return super.pushViewController(viewController, animated: animated)
  253. }
  254. var alongsideTransition: (() -> Void)? = nil
  255. if dismissOnPush {
  256. alongsideTransition = { [weak self] in
  257. guard let self = self else { return }
  258. self.dismissAnimation(animated: animated || self.alwaysAnimate)
  259. }
  260. }
  261. let pushed = SideMenuPushCoordinator(config:
  262. .init(
  263. allowPushOfSameClassTwice: allowPushOfSameClassTwice,
  264. alongsideTransition: alongsideTransition,
  265. animated: animated,
  266. fromViewController: self,
  267. pushStyle: pushStyle,
  268. toViewController: viewController
  269. )
  270. ).start()
  271. if !pushed {
  272. super.pushViewController(viewController, animated: animated)
  273. }
  274. }
  275. override open var transitioningDelegate: UIViewControllerTransitioningDelegate? {
  276. get {
  277. guard transitionController == nil else { return transitionController }
  278. transitionController = SideMenuTransitionController(leftSide: leftSide, config: settings)
  279. transitionController?.delegate = self
  280. transitionController?.interactive = transitionInteractive
  281. transitionInteractive = false
  282. return transitionController
  283. }
  284. set { Print.warning(.transitioningDelegate, required: true) }
  285. }
  286. }
  287. // Interface
  288. extension SideMenuNavigationController: Model {
  289. @IBInspectable open var allowPushOfSameClassTwice: Bool {
  290. get { return settings.allowPushOfSameClassTwice }
  291. set { settings.allowPushOfSameClassTwice = newValue }
  292. }
  293. @IBInspectable open var alwaysAnimate: Bool {
  294. get { return settings.alwaysAnimate }
  295. set { settings.alwaysAnimate = newValue }
  296. }
  297. @IBInspectable open var animationOptions: UIView.AnimationOptions {
  298. get { return settings.animationOptions }
  299. set { settings.animationOptions = newValue }
  300. }
  301. open var blurEffectStyle: UIBlurEffect.Style? {
  302. get { return settings.blurEffectStyle }
  303. set { settings.blurEffectStyle = newValue }
  304. }
  305. @IBInspectable open var completeGestureDuration: Double {
  306. get { return settings.completeGestureDuration }
  307. set { settings.completeGestureDuration = newValue }
  308. }
  309. @IBInspectable open var completionCurve: UIView.AnimationCurve {
  310. get { return settings.completionCurve }
  311. set { settings.completionCurve = newValue }
  312. }
  313. @IBInspectable open var dismissDuration: Double {
  314. get { return settings.dismissDuration }
  315. set { settings.dismissDuration = newValue }
  316. }
  317. @IBInspectable open var dismissOnPresent: Bool {
  318. get { return settings.dismissOnPresent }
  319. set { settings.dismissOnPresent = newValue }
  320. }
  321. @IBInspectable open var dismissOnPush: Bool {
  322. get { return settings.dismissOnPush }
  323. set { settings.dismissOnPush = newValue }
  324. }
  325. @IBInspectable open var dismissOnRotation: Bool {
  326. get { return settings.dismissOnRotation }
  327. set { settings.dismissOnRotation = newValue }
  328. }
  329. @IBInspectable open var dismissWhenBackgrounded: Bool {
  330. get { return settings.dismissWhenBackgrounded }
  331. set { settings.dismissWhenBackgrounded = newValue }
  332. }
  333. @IBInspectable open var enableSwipeToDismissGesture: Bool {
  334. get { return settings.enableSwipeToDismissGesture }
  335. set { settings.enableSwipeToDismissGesture = newValue }
  336. }
  337. @IBInspectable open var enableTapToDismissGesture: Bool {
  338. get { return settings.enableTapToDismissGesture }
  339. set { settings.enableTapToDismissGesture = newValue }
  340. }
  341. @IBInspectable open var initialSpringVelocity: CGFloat {
  342. get { return settings.initialSpringVelocity }
  343. set { settings.initialSpringVelocity = newValue }
  344. }
  345. /// Whether the menu appears on the right or left side of the screen. Right is the default. This property cannot be changed after the menu has loaded.
  346. @IBInspectable open var leftSide: Bool {
  347. get { return _leftSide.value }
  348. set { _leftSide.value = newValue }
  349. }
  350. /// Indicates if the menu is anywhere in the view hierarchy, even if covered by another view controller.
  351. open override var isHidden: Bool {
  352. return super.isHidden
  353. }
  354. @IBInspectable open var menuWidth: CGFloat {
  355. get { return settings.menuWidth }
  356. set { settings.menuWidth = newValue }
  357. }
  358. @IBInspectable open var presentingViewControllerUserInteractionEnabled: Bool {
  359. get { return settings.presentingViewControllerUserInteractionEnabled }
  360. set { settings.presentingViewControllerUserInteractionEnabled = newValue }
  361. }
  362. @IBInspectable open var presentingViewControllerUseSnapshot: Bool {
  363. get { return settings.presentingViewControllerUseSnapshot }
  364. set { settings.presentingViewControllerUseSnapshot = newValue }
  365. }
  366. @IBInspectable open var presentDuration: Double {
  367. get { return settings.presentDuration }
  368. set { settings.presentDuration = newValue }
  369. }
  370. open var presentationStyle: SideMenuPresentationStyle {
  371. get { return settings.presentationStyle }
  372. set { settings.presentationStyle = newValue }
  373. }
  374. @IBInspectable open var pushStyle: SideMenuPushStyle {
  375. get { return settings.pushStyle }
  376. set { settings.pushStyle = newValue }
  377. }
  378. @IBInspectable open var statusBarEndAlpha: CGFloat {
  379. get { return settings.statusBarEndAlpha }
  380. set { settings.statusBarEndAlpha = newValue }
  381. }
  382. @IBInspectable open var usingSpringWithDamping: CGFloat {
  383. get { return settings.usingSpringWithDamping }
  384. set { settings.usingSpringWithDamping = newValue }
  385. }
  386. }
  387. extension SideMenuNavigationController: SideMenuTransitionControllerDelegate {
  388. func sideMenuTransitionController(_ transitionController: SideMenuTransitionController, didDismiss viewController: UIViewController) {
  389. sideMenuManager.sideMenuTransitionDidDismiss(menu: self)
  390. }
  391. func sideMenuTransitionController(_ transitionController: SideMenuTransitionController, didPresent viewController: UIViewController) {
  392. swipeToDismissGesture?.remove()
  393. swipeToDismissGesture = addSwipeToDismissGesture(to: view.superview)
  394. tapToDismissGesture = addTapToDismissGesture(to: view.superview)
  395. }
  396. }
  397. internal extension SideMenuNavigationController {
  398. func handleMenuPan(_ gesture: UIPanGestureRecognizer, _ presenting: Bool) {
  399. let width = menuWidth
  400. let distance = gesture.xTranslation / width
  401. let progress = max(min(distance * factor(presenting), 1), 0)
  402. switch (gesture.state) {
  403. case .began:
  404. if !presenting {
  405. dismissMenu(interactively: true)
  406. }
  407. fallthrough
  408. case .changed:
  409. transitionController?.handle(state: .update(progress: progress))
  410. case .ended:
  411. let velocity = gesture.xVelocity * factor(presenting)
  412. let finished = velocity >= 100 || velocity >= -50 && abs(progress) >= 0.5
  413. transitionController?.handle(state: finished ? .finish : .cancel)
  414. default:
  415. transitionController?.handle(state: .cancel)
  416. }
  417. }
  418. func cancelMenuPan(_ gesture: UIPanGestureRecognizer) {
  419. transitionController?.handle(state: .cancel)
  420. }
  421. func dismissMenu(animated flag: Bool = true, interactively: Bool = false, completion: (() -> Void)? = nil) {
  422. guard !isHidden else { return }
  423. transitionController?.interactive = interactively
  424. dismiss(animated: flag, completion: completion)
  425. }
  426. // Note: although this method is syntactically reversed it allows the interactive property to scoped privately
  427. func present(from viewController: UIViewController?, interactively: Bool, completion: (() -> Void)? = nil) {
  428. guard let viewController = viewController else { return }
  429. transitionInteractive = interactively
  430. viewController.present(self, animated: true, completion: completion)
  431. }
  432. }
  433. private extension SideMenuNavigationController {
  434. weak var activeDelegate: SideMenuNavigationControllerDelegate? {
  435. guard !view.isHidden else { return nil }
  436. if let sideMenuDelegate = sideMenuDelegate { return sideMenuDelegate }
  437. return findViewController as? SideMenuNavigationControllerDelegate
  438. }
  439. var findViewController: UIViewController? {
  440. foundViewController = foundViewController ?? presentingViewController?.activeViewController
  441. return foundViewController
  442. }
  443. func dismissAnimation(animated: Bool) {
  444. transitionController?.transition(presenting: false, animated: animated, alongsideTransition: { [weak self] in
  445. guard let self = self else { return }
  446. self.activeDelegate?.sideMenuWillDisappear?(menu: self, animated: animated)
  447. }, completion: { [weak self] _ in
  448. guard let self = self else { return }
  449. self.activeDelegate?.sideMenuDidDisappear?(menu: self, animated: animated)
  450. self.dismiss(animated: false, completion: nil)
  451. self.foundViewController = nil
  452. })
  453. }
  454. func setup() {
  455. modalPresentationStyle = .overFullScreen
  456. setupBlur()
  457. if #available(iOS 13.0, *) {} else {
  458. registerForNotifications()
  459. }
  460. }
  461. func setupBlur() {
  462. removeBlur()
  463. guard let blurEffectStyle = blurEffectStyle,
  464. let view = topViewController?.view,
  465. !UIAccessibility.isReduceTransparencyEnabled else {
  466. return
  467. }
  468. originalBackgroundColor = originalBackgroundColor ?? view.backgroundColor
  469. let blurEffect = UIBlurEffect(style: blurEffectStyle)
  470. let blurView = UIVisualEffectView(effect: blurEffect)
  471. view.backgroundColor = UIColor.clear
  472. if let tableViewController = topViewController as? UITableViewController {
  473. tableViewController.tableView.backgroundView = blurView
  474. tableViewController.tableView.separatorEffect = UIVibrancyEffect(blurEffect: blurEffect)
  475. tableViewController.tableView.reloadData()
  476. } else {
  477. blurView.autoresizingMask = [.flexibleHeight, .flexibleWidth]
  478. blurView.frame = view.bounds
  479. view.insertSubview(blurView, at: 0)
  480. }
  481. }
  482. func removeBlur() {
  483. guard let originalBackgroundColor = originalBackgroundColor,
  484. let view = topViewController?.view else {
  485. return
  486. }
  487. self.originalBackgroundColor = nil
  488. view.backgroundColor = originalBackgroundColor
  489. if let tableViewController = topViewController as? UITableViewController {
  490. tableViewController.tableView.backgroundView = nil
  491. tableViewController.tableView.separatorEffect = nil
  492. tableViewController.tableView.reloadData()
  493. } else if let blurView = view.subviews.first as? UIVisualEffectView {
  494. blurView.removeFromSuperview()
  495. }
  496. }
  497. @available(iOS, deprecated: 13.0)
  498. func registerForNotifications() {
  499. NotificationCenter.default.removeObserver(self)
  500. [UIApplication.willChangeStatusBarFrameNotification,
  501. UIApplication.didEnterBackgroundNotification].forEach {
  502. NotificationCenter.default.addObserver(self, selector: #selector(handleNotification), name: $0, object: nil)
  503. }
  504. }
  505. @available(iOS, deprecated: 13.0)
  506. @objc func handleNotification(notification: NSNotification) {
  507. guard isHidden else { return }
  508. switch notification.name {
  509. case UIApplication.willChangeStatusBarFrameNotification:
  510. // Dismiss for in-call status bar changes but not rotation
  511. if !rotating {
  512. dismissMenu()
  513. }
  514. case UIApplication.didEnterBackgroundNotification:
  515. if dismissWhenBackgrounded {
  516. dismissMenu()
  517. }
  518. default: break
  519. }
  520. }
  521. @discardableResult func addSwipeToDismissGesture(to view: UIView?) -> UIPanGestureRecognizer? {
  522. guard enableSwipeToDismissGesture else { return nil }
  523. return UIPanGestureRecognizer(addTo: view, target: self, action: #selector(handleDismissMenuPan(_:)))?.with {
  524. $0.cancelsTouchesInView = false
  525. }
  526. }
  527. @discardableResult func addTapToDismissGesture(to view: UIView?) -> UITapGestureRecognizer? {
  528. guard enableTapToDismissGesture else { return nil }
  529. return UITapGestureRecognizer(addTo: view, target: self, action: #selector(handleDismissMenuTap(_:)))?.with {
  530. $0.cancelsTouchesInView = false
  531. }
  532. }
  533. @objc func handleDismissMenuTap(_ tap: UITapGestureRecognizer) {
  534. let hitTest = view.window?.hitTest(tap.location(in: view.superview), with: nil)
  535. guard hitTest == view.superview else { return }
  536. dismissMenu()
  537. }
  538. @objc func handleDismissMenuPan(_ gesture: UIPanGestureRecognizer) {
  539. handleMenuPan(gesture, false)
  540. }
  541. func factor(_ presenting: Bool) -> CGFloat {
  542. return presenting ? presentFactor : hideFactor
  543. }
  544. var presentFactor: CGFloat {
  545. return leftSide ? 1 : -1
  546. }
  547. var hideFactor: CGFloat {
  548. return -presentFactor
  549. }
  550. }