2 // UISideMenuNavigationController.swift
4 // Created by Jon Kent on 1/14/16.
5 // Copyright © 2016 Jon Kent. All rights reserved.
10 @objc public protocol UISideMenuNavigationControllerDelegate {
11 @objc optional func sideMenuWillAppear(menu: UISideMenuNavigationController, animated: Bool)
12 @objc optional func sideMenuDidAppear(menu: UISideMenuNavigationController, animated: Bool)
13 @objc optional func sideMenuWillDisappear(menu: UISideMenuNavigationController, animated: Bool)
14 @objc optional func sideMenuDidDisappear(menu: UISideMenuNavigationController, animated: Bool)
18 open class UISideMenuNavigationController: UINavigationController {
20 fileprivate weak var foundDelegate: UISideMenuNavigationControllerDelegate?
21 fileprivate weak var activeDelegate: UISideMenuNavigationControllerDelegate? {
23 guard !view.isHidden else {
27 return sideMenuDelegate ?? foundDelegate ?? findDelegate(forViewController: presentingViewController)
30 fileprivate func findDelegate(forViewController: UIViewController?) -> UISideMenuNavigationControllerDelegate? {
31 if let navigationController = forViewController as? UINavigationController {
32 return findDelegate(forViewController: navigationController.topViewController)
34 if let tabBarController = forViewController as? UITabBarController {
35 return findDelegate(forViewController: tabBarController.selectedViewController)
37 if let splitViewController = forViewController as? UISplitViewController {
38 return findDelegate(forViewController: splitViewController.viewControllers.last)
41 foundDelegate = forViewController as? UISideMenuNavigationControllerDelegate
44 fileprivate var usingInterfaceBuilder = false
45 internal var locked = false
46 internal var originalMenuBackgroundColor: UIColor?
47 internal var transition: SideMenuTransition {
49 return sideMenuManager.transition
53 /// Delegate for receiving appear and disappear related events. If `nil` the visible view controller that displays a `UISideMenuNavigationController` automatically receives these events.
54 open weak var sideMenuDelegate: UISideMenuNavigationControllerDelegate?
56 /// SideMenuManager instance associated with this menu. Default is `SideMenuManager.default`. This property cannot be changed after the menu has loaded.
57 open weak var sideMenuManager: SideMenuManager! = SideMenuManager.default {
59 if locked && oldValue != nil {
60 print("SideMenu Warning: a menu's sideMenuManager property cannot be changed after it has loaded.")
61 sideMenuManager = oldValue
66 /// 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.
67 @IBInspectable open var menuWidth: CGFloat = 0 {
69 if !isHidden && oldValue != menuWidth {
70 print("SideMenu Warning: a menu's width property can only be changed when it is hidden.")
76 /// 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.
77 @IBInspectable open var leftSide: Bool = false {
79 if locked && leftSide != oldValue {
80 print("SideMenu Warning: a menu's leftSide property cannot be changed after it has loaded.")
86 /// Indicates if the menu is anywhere in the view hierarchy, even if covered by another view controller.
87 open var isHidden: Bool {
89 return self.presentingViewController == nil
94 // This override prevents newbie developers from creating black/blank menus and opening newbie issues.
95 // If you would like to remove this override, define STFU_SIDEMENU in the Active Compilation Conditions of your .plist file.
96 // Sorry for the inconvenience experienced developers :(
97 @available(*, unavailable, renamed: "init(rootViewController:)")
99 fatalError("init is not available")
102 public override init(rootViewController: UIViewController) {
103 super.init(rootViewController: rootViewController)
106 public override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) {
107 super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
110 public required init?(coder aDecoder: NSCoder) {
111 super.init(coder: aDecoder)
115 open override func awakeFromNib() {
118 usingInterfaceBuilder = true
121 override open func viewDidLoad() {
124 if !locked && usingInterfaceBuilder {
126 sideMenuManager.menuLeftNavigationController = self
128 sideMenuManager.menuRightNavigationController = self
133 open override func viewWillAppear(_ animated: Bool) {
134 super.viewWillAppear(animated)
136 // Dismiss keyboard to prevent weird keyboard animations from occurring during transition
137 presentingViewController?.view.endEditing(true)
140 activeDelegate?.sideMenuWillAppear?(menu: self, animated: animated)
143 override open func viewDidAppear(_ animated: Bool) {
144 super.viewDidAppear(animated)
146 // We had presented a view before, so lets dismiss ourselves as already acted upon
148 transition.hideMenuComplete()
149 dismiss(animated: false, completion: { () -> Void in
150 self.view.isHidden = false
156 activeDelegate?.sideMenuDidAppear?(menu: self, animated: animated)
159 if topViewController == nil {
160 print("SideMenu Warning: the menu doesn't have a view controller to show! UISideMenuNavigationController needs a view controller to display just like a UINavigationController.")
165 override open func viewWillDisappear(_ animated: Bool) {
166 super.viewWillDisappear(animated)
168 // When presenting a view controller from the menu, the menu view gets moved into another transition view above our transition container
169 // which can break the visual layout we had before. So, we move the menu view back to its original transition view to preserve it.
170 if !isBeingDismissed {
171 guard let sideMenuManager = sideMenuManager else {
175 if let mainView = transition.mainViewController?.view {
176 switch sideMenuManager.menuPresentMode {
177 case .viewSlideOut, .viewSlideInOut:
178 mainView.superview?.insertSubview(view, belowSubview: mainView)
179 case .menuSlideIn, .menuDissolveIn:
180 if let tapView = transition.tapView {
181 mainView.superview?.insertSubview(view, aboveSubview: tapView)
183 mainView.superview?.insertSubview(view, aboveSubview: mainView)
188 // 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.
189 UIView.animate(withDuration: animated ? sideMenuManager.menuAnimationDismissDuration : 0,
191 usingSpringWithDamping: sideMenuManager.menuAnimationUsingSpringWithDamping,
192 initialSpringVelocity: sideMenuManager.menuAnimationInitialSpringVelocity,
193 options: sideMenuManager.menuAnimationOptions,
195 self.transition.hideMenuStart()
196 self.activeDelegate?.sideMenuWillDisappear?(menu: self, animated: animated)
197 }) { (finished) -> Void in
198 self.activeDelegate?.sideMenuDidDisappear?(menu: self, animated: animated)
199 self.view.isHidden = true
205 activeDelegate?.sideMenuWillDisappear?(menu: self, animated: animated)
208 override open func viewDidDisappear(_ animated: Bool) {
209 super.viewDidDisappear(animated)
211 // Work-around: if the menu is dismissed without animation the transition logic is never called to restore the
212 // the view hierarchy leaving the screen black/empty. This is because the transition moves views within a container
213 // view, but dismissing without animation removes the container view before the original hierarchy is restored.
214 // This check corrects that.
215 if let sideMenuDelegate = activeDelegate as? UIViewController, sideMenuDelegate.view.window == nil {
216 transition.hideMenuStart().hideMenuComplete()
219 activeDelegate?.sideMenuDidDisappear?(menu: self, animated: animated)
221 // Clear selecton on UITableViewControllers when reappearing using custom transitions
222 guard let tableViewController = topViewController as? UITableViewController,
223 let tableView = tableViewController.tableView,
224 let indexPaths = tableView.indexPathsForSelectedRows,
225 tableViewController.clearsSelectionOnViewWillAppear else {
229 for indexPath in indexPaths {
230 tableView.deselectRow(at: indexPath, animated: false)
234 override open func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
235 super.viewWillTransition(to: size, with: coordinator)
237 // Don't bother resizing if the view isn't visible
238 guard !view.isHidden else {
242 NotificationCenter.default.removeObserver(self.transition, name: NSNotification.Name.UIApplicationWillChangeStatusBarFrame, object: nil)
243 coordinator.animate(alongsideTransition: { (context) in
244 self.transition.presentMenuStart()
246 NotificationCenter.default.addObserver(self.transition, selector:#selector(SideMenuTransition.handleNotification), name: NSNotification.Name.UIApplicationWillChangeStatusBarFrame, object: nil)
250 override open func pushViewController(_ viewController: UIViewController, animated: Bool) {
251 guard let sideMenuManager = sideMenuManager, viewControllers.count > 0 && sideMenuManager.menuPushStyle != .subMenu else {
252 // NOTE: pushViewController is called by init(rootViewController: UIViewController)
253 // so we must perform the normal super method in this case.
254 super.pushViewController(viewController, animated: animated)
258 let splitViewController = presentingViewController as? UISplitViewController
259 let tabBarController = presentingViewController as? UITabBarController
260 let potentialNavigationController = (splitViewController?.viewControllers.first ?? tabBarController?.selectedViewController) ?? presentingViewController
261 guard let navigationController = potentialNavigationController as? UINavigationController else {
262 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.")
266 let activeDelegate = self.activeDelegate
269 // To avoid overlapping dismiss & pop/push calls, create a transaction block where the menu
270 // is dismissed after showing the appropriate screen
271 CATransaction.begin()
272 if sideMenuManager.menuDismissOnPush {
273 let animated = animated || sideMenuManager.menuAlwaysAnimate
275 CATransaction.setCompletionBlock( { () -> Void in
276 activeDelegate?.sideMenuDidDisappear?(menu: self, animated: animated)
278 self.transition.hideMenuStart().hideMenuComplete()
280 self.dismiss(animated: animated, completion: nil)
284 let areAnimationsEnabled = UIView.areAnimationsEnabled
285 UIView.setAnimationsEnabled(true)
286 UIView.animate(withDuration: sideMenuManager.menuAnimationDismissDuration,
288 usingSpringWithDamping: sideMenuManager.menuAnimationUsingSpringWithDamping,
289 initialSpringVelocity: sideMenuManager.menuAnimationInitialSpringVelocity,
290 options: sideMenuManager.menuAnimationOptions,
292 activeDelegate?.sideMenuWillDisappear?(menu: self, animated: animated)
293 self.transition.hideMenuStart()
295 UIView.setAnimationsEnabled(areAnimationsEnabled)
299 if let lastViewController = navigationController.viewControllers.last, !sideMenuManager.menuAllowPushOfSameClassTwice && type(of: lastViewController) == type(of: viewController) {
300 CATransaction.commit()
304 switch sideMenuManager.menuPushStyle {
305 case .subMenu, .defaultBehavior: break // .subMenu handled earlier, .defaultBehavior falls through to end
306 case .popWhenPossible:
307 for subViewController in navigationController.viewControllers.reversed() {
308 if type(of: subViewController) == type(of: viewController) {
309 navigationController.popToViewController(subViewController, animated: animated)
310 CATransaction.commit()
314 case .preserve, .preserveAndHideBackButton:
315 var viewControllers = navigationController.viewControllers
316 let filtered = viewControllers.filter { preservedViewController in type(of: preservedViewController) == type(of: viewController) }
317 if let preservedViewController = filtered.last {
318 viewControllers = viewControllers.filter { subViewController in subViewController !== preservedViewController }
319 if sideMenuManager.menuPushStyle == .preserveAndHideBackButton {
320 preservedViewController.navigationItem.hidesBackButton = true
322 viewControllers.append(preservedViewController)
323 navigationController.setViewControllers(viewControllers, animated: animated)
324 CATransaction.commit()
327 if sideMenuManager.menuPushStyle == .preserveAndHideBackButton {
328 viewController.navigationItem.hidesBackButton = true
331 viewController.navigationItem.hidesBackButton = true
332 navigationController.setViewControllers([viewController], animated: animated)
333 CATransaction.commit()
337 navigationController.pushViewController(viewController, animated: animated)
338 CATransaction.commit()