344 lines
16 KiB
Swift
344 lines
16 KiB
Swift
//
|
|
// 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()
|
|
}
|
|
|
|
}
|
|
|
|
|