--- /dev/null
+//
+// UISideMenuNavigationController.swift
+//
+// Created by Jon Kent on 1/14/16.
+// Copyright © 2016 Jon Kent. All rights reserved.
+//
+
+import UIKit
+
+@objc public protocol UISideMenuNavigationControllerDelegate {
+ @objc optional func sideMenuWillAppear(menu: UISideMenuNavigationController, animated: Bool)
+ @objc optional func sideMenuDidAppear(menu: UISideMenuNavigationController, animated: Bool)
+ @objc optional func sideMenuWillDisappear(menu: UISideMenuNavigationController, animated: Bool)
+ @objc optional func sideMenuDidDisappear(menu: UISideMenuNavigationController, animated: Bool)
+}
+
+@objcMembers
+open class UISideMenuNavigationController: UINavigationController {
+
+ fileprivate weak var foundDelegate: UISideMenuNavigationControllerDelegate?
+ fileprivate weak var activeDelegate: UISideMenuNavigationControllerDelegate? {
+ get {
+ guard !view.isHidden else {
+ return nil
+ }
+
+ return sideMenuDelegate ?? foundDelegate ?? findDelegate(forViewController: presentingViewController)
+ }
+ }
+ fileprivate func findDelegate(forViewController: UIViewController?) -> UISideMenuNavigationControllerDelegate? {
+ if let navigationController = forViewController as? UINavigationController {
+ return findDelegate(forViewController: navigationController.topViewController)
+ }
+ if let tabBarController = forViewController as? UITabBarController {
+ return findDelegate(forViewController: tabBarController.selectedViewController)
+ }
+ if let splitViewController = forViewController as? UISplitViewController {
+ return findDelegate(forViewController: splitViewController.viewControllers.last)
+ }
+
+ foundDelegate = forViewController as? UISideMenuNavigationControllerDelegate
+ return foundDelegate
+ }
+ fileprivate var usingInterfaceBuilder = false
+ internal var locked = false
+ internal var originalMenuBackgroundColor: UIColor?
+ internal var transition: SideMenuTransition {
+ get {
+ return sideMenuManager.transition
+ }
+ }
+
+ /// Delegate for receiving appear and disappear related events. If `nil` the visible view controller that displays a `UISideMenuNavigationController` automatically receives these events.
+ open weak var sideMenuDelegate: UISideMenuNavigationControllerDelegate?
+
+ /// SideMenuManager instance associated with this menu. Default is `SideMenuManager.default`. This property cannot be changed after the menu has loaded.
+ open weak var sideMenuManager: SideMenuManager! = SideMenuManager.default {
+ didSet {
+ if locked && oldValue != nil {
+ print("SideMenu Warning: a menu's sideMenuManager property cannot be changed after it has loaded.")
+ sideMenuManager = oldValue
+ }
+ }
+ }
+
+ /// Width of the menu when presented on screen, showing the existing view controller in the remaining space. Default is zero. When zero, `sideMenuManager.menuWidth` is used. This property cannot be changed while the isHidden property is false.
+ @IBInspectable open var menuWidth: CGFloat = 0 {
+ didSet {
+ if !isHidden && oldValue != menuWidth {
+ print("SideMenu Warning: a menu's width property can only be changed when it is hidden.")
+ menuWidth = oldValue
+ }
+ }
+ }
+
+ /// 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.
+ @IBInspectable open var leftSide: Bool = false {
+ didSet {
+ if locked && leftSide != oldValue {
+ print("SideMenu Warning: a menu's leftSide property cannot be changed after it has loaded.")
+ leftSide = oldValue
+ }
+ }
+ }
+
+ /// Indicates if the menu is anywhere in the view hierarchy, even if covered by another view controller.
+ open var isHidden: Bool {
+ get {
+ return self.presentingViewController == nil
+ }
+ }
+
+ #if !STFU_SIDEMENU
+ // This override prevents newbie developers from creating black/blank menus and opening newbie issues.
+ // If you would like to remove this override, define STFU_SIDEMENU in the Active Compilation Conditions of your .plist file.
+ // Sorry for the inconvenience experienced developers :(
+ @available(*, unavailable, renamed: "init(rootViewController:)")
+ public init() {
+ fatalError("init is not available")
+ }
+
+ public override init(rootViewController: UIViewController) {
+ super.init(rootViewController: rootViewController)
+ }
+
+ public override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) {
+ super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
+ }
+
+ public required init?(coder aDecoder: NSCoder) {
+ super.init(coder: aDecoder)
+ }
+ #endif
+
+ open override func awakeFromNib() {
+ super.awakeFromNib()
+
+ usingInterfaceBuilder = true
+ }
+
+ override open func viewDidLoad() {
+ super.viewDidLoad()
+
+ if !locked && usingInterfaceBuilder {
+ if leftSide {
+ sideMenuManager.menuLeftNavigationController = self
+ } else {
+ sideMenuManager.menuRightNavigationController = self
+ }
+ }
+ }
+
+ open override func viewWillAppear(_ animated: Bool) {
+ super.viewWillAppear(animated)
+
+ // Dismiss keyboard to prevent weird keyboard animations from occurring during transition
+ presentingViewController?.view.endEditing(true)
+
+ foundDelegate = nil
+ activeDelegate?.sideMenuWillAppear?(menu: self, animated: animated)
+ }
+
+ override open func viewDidAppear(_ animated: Bool) {
+ super.viewDidAppear(animated)
+
+ // We had presented a view before, so lets dismiss ourselves as already acted upon
+ if view.isHidden {
+ transition.hideMenuComplete()
+ dismiss(animated: false, completion: { () -> Void in
+ self.view.isHidden = false
+ })
+
+ return
+ }
+
+ activeDelegate?.sideMenuDidAppear?(menu: self, animated: animated)
+
+ #if !STFU_SIDEMENU
+ if topViewController == nil {
+ print("SideMenu Warning: the menu doesn't have a view controller to show! UISideMenuNavigationController needs a view controller to display just like a UINavigationController.")
+ }
+ #endif
+ }
+
+ override open func viewWillDisappear(_ animated: Bool) {
+ super.viewWillDisappear(animated)
+
+ // When presenting a view controller from the menu, the menu view gets moved into another transition view above our transition container
+ // which can break the visual layout we had before. So, we move the menu view back to its original transition view to preserve it.
+ if !isBeingDismissed {
+ guard let sideMenuManager = sideMenuManager else {
+ return
+ }
+
+ if let mainView = transition.mainViewController?.view {
+ switch sideMenuManager.menuPresentMode {
+ case .viewSlideOut, .viewSlideInOut:
+ mainView.superview?.insertSubview(view, belowSubview: mainView)
+ case .menuSlideIn, .menuDissolveIn:
+ if let tapView = transition.tapView {
+ mainView.superview?.insertSubview(view, aboveSubview: tapView)
+ } else {
+ mainView.superview?.insertSubview(view, aboveSubview: mainView)
+ }
+ }
+ }
+
+ // 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.
+ UIView.animate(withDuration: animated ? sideMenuManager.menuAnimationDismissDuration : 0,
+ delay: 0,
+ usingSpringWithDamping: sideMenuManager.menuAnimationUsingSpringWithDamping,
+ initialSpringVelocity: sideMenuManager.menuAnimationInitialSpringVelocity,
+ options: sideMenuManager.menuAnimationOptions,
+ animations: {
+ self.transition.hideMenuStart()
+ self.activeDelegate?.sideMenuWillDisappear?(menu: self, animated: animated)
+ }) { (finished) -> Void in
+ self.activeDelegate?.sideMenuDidDisappear?(menu: self, animated: animated)
+ self.view.isHidden = true
+ }
+
+ return
+ }
+
+ activeDelegate?.sideMenuWillDisappear?(menu: self, animated: animated)
+ }
+
+ override open func viewDidDisappear(_ animated: Bool) {
+ super.viewDidDisappear(animated)
+
+ // Work-around: if the menu is dismissed without animation the transition logic is never called to restore the
+ // the view hierarchy leaving the screen black/empty. This is because the transition moves views within a container
+ // view, but dismissing without animation removes the container view before the original hierarchy is restored.
+ // This check corrects that.
+ if let sideMenuDelegate = activeDelegate as? UIViewController, sideMenuDelegate.view.window == nil {
+ transition.hideMenuStart().hideMenuComplete()
+ }
+
+ activeDelegate?.sideMenuDidDisappear?(menu: self, animated: animated)
+
+ // Clear selecton on UITableViewControllers when reappearing using custom transitions
+ guard let tableViewController = topViewController as? UITableViewController,
+ let tableView = tableViewController.tableView,
+ let indexPaths = tableView.indexPathsForSelectedRows,
+ tableViewController.clearsSelectionOnViewWillAppear else {
+ return
+ }
+
+ for indexPath in indexPaths {
+ tableView.deselectRow(at: indexPath, animated: false)
+ }
+ }
+
+ override open func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
+ super.viewWillTransition(to: size, with: coordinator)
+
+ // Don't bother resizing if the view isn't visible
+ guard !view.isHidden else {
+ return
+ }
+
+ NotificationCenter.default.removeObserver(self.transition, name: NSNotification.Name.UIApplicationWillChangeStatusBarFrame, object: nil)
+ coordinator.animate(alongsideTransition: { (context) in
+ self.transition.presentMenuStart()
+ }) { (context) in
+ NotificationCenter.default.addObserver(self.transition, selector:#selector(SideMenuTransition.handleNotification), name: NSNotification.Name.UIApplicationWillChangeStatusBarFrame, object: nil)
+ }
+ }
+
+ override open func pushViewController(_ viewController: UIViewController, animated: Bool) {
+ guard let sideMenuManager = sideMenuManager, viewControllers.count > 0 && sideMenuManager.menuPushStyle != .subMenu else {
+ // NOTE: pushViewController is called by init(rootViewController: UIViewController)
+ // so we must perform the normal super method in this case.
+ super.pushViewController(viewController, animated: animated)
+ return
+ }
+
+ let splitViewController = presentingViewController as? UISplitViewController
+ let tabBarController = presentingViewController as? UITabBarController
+ let potentialNavigationController = (splitViewController?.viewControllers.first ?? tabBarController?.selectedViewController) ?? presentingViewController
+ guard let navigationController = potentialNavigationController as? UINavigationController else {
+ print("SideMenu Warning: attempt to push a View Controller from \(String(describing: potentialNavigationController.self)) where its navigationController == nil. It must be embedded in a Navigation Controller for this to work.")
+ return
+ }
+
+ let activeDelegate = self.activeDelegate
+ foundDelegate = nil
+
+ // To avoid overlapping dismiss & pop/push calls, create a transaction block where the menu
+ // is dismissed after showing the appropriate screen
+ CATransaction.begin()
+ if sideMenuManager.menuDismissOnPush {
+ let animated = animated || sideMenuManager.menuAlwaysAnimate
+
+ CATransaction.setCompletionBlock( { () -> Void in
+ activeDelegate?.sideMenuDidDisappear?(menu: self, animated: animated)
+ if !animated {
+ self.transition.hideMenuStart().hideMenuComplete()
+ }
+ self.dismiss(animated: animated, completion: nil)
+ })
+
+ if animated {
+ let areAnimationsEnabled = UIView.areAnimationsEnabled
+ UIView.setAnimationsEnabled(true)
+ UIView.animate(withDuration: sideMenuManager.menuAnimationDismissDuration,
+ delay: 0,
+ usingSpringWithDamping: sideMenuManager.menuAnimationUsingSpringWithDamping,
+ initialSpringVelocity: sideMenuManager.menuAnimationInitialSpringVelocity,
+ options: sideMenuManager.menuAnimationOptions,
+ animations: {
+ activeDelegate?.sideMenuWillDisappear?(menu: self, animated: animated)
+ self.transition.hideMenuStart()
+ })
+ UIView.setAnimationsEnabled(areAnimationsEnabled)
+ }
+ }
+
+ if let lastViewController = navigationController.viewControllers.last, !sideMenuManager.menuAllowPushOfSameClassTwice && type(of: lastViewController) == type(of: viewController) {
+ CATransaction.commit()
+ return
+ }
+
+ switch sideMenuManager.menuPushStyle {
+ case .subMenu, .defaultBehavior: break // .subMenu handled earlier, .defaultBehavior falls through to end
+ case .popWhenPossible:
+ for subViewController in navigationController.viewControllers.reversed() {
+ if type(of: subViewController) == type(of: viewController) {
+ navigationController.popToViewController(subViewController, animated: animated)
+ CATransaction.commit()
+ return
+ }
+ }
+ case .preserve, .preserveAndHideBackButton:
+ var viewControllers = navigationController.viewControllers
+ let filtered = viewControllers.filter { preservedViewController in type(of: preservedViewController) == type(of: viewController) }
+ if let preservedViewController = filtered.last {
+ viewControllers = viewControllers.filter { subViewController in subViewController !== preservedViewController }
+ if sideMenuManager.menuPushStyle == .preserveAndHideBackButton {
+ preservedViewController.navigationItem.hidesBackButton = true
+ }
+ viewControllers.append(preservedViewController)
+ navigationController.setViewControllers(viewControllers, animated: animated)
+ CATransaction.commit()
+ return
+ }
+ if sideMenuManager.menuPushStyle == .preserveAndHideBackButton {
+ viewController.navigationItem.hidesBackButton = true
+ }
+ case .replace:
+ viewController.navigationItem.hidesBackButton = true
+ navigationController.setViewControllers([viewController], animated: animated)
+ CATransaction.commit()
+ return
+ }
+
+ navigationController.pushViewController(viewController, animated: animated)
+ CATransaction.commit()
+ }
+
+}
+
+