123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656 |
- import UIKit
- @objc public enum SideMenuPushStyle: Int { case
- `default`,
- popWhenPossible,
- preserve,
- preserveAndHideBackButton,
- replace,
- subMenu
- internal var hidesBackButton: Bool {
- switch self {
- case .preserveAndHideBackButton, .replace: return true
- case .default, .popWhenPossible, .preserve, .subMenu: return false
- }
- }
- }
- internal protocol MenuModel {
-
- var allowPushOfSameClassTwice: Bool { get }
-
- var alwaysAnimate: Bool { get }
-
- var blurEffectStyle: UIBlurEffect.Style? { get }
-
- var completionCurve: UIView.AnimationCurve { get }
-
- var dismissOnPresent: Bool { get }
-
- var dismissOnPush: Bool { get }
-
- var dismissOnRotation: Bool { get }
-
- var dismissWhenBackgrounded: Bool { get }
-
- var enableSwipeToDismissGesture: Bool { get }
-
- var enableTapToDismissGesture: Bool { get }
-
- var pushStyle: SideMenuPushStyle { get }
- }
- @objc public protocol SideMenuNavigationControllerDelegate {
- @objc optional func sideMenuWillAppear(menu: SideMenuNavigationController, animated: Bool)
- @objc optional func sideMenuDidAppear(menu: SideMenuNavigationController, animated: Bool)
- @objc optional func sideMenuWillDisappear(menu: SideMenuNavigationController, animated: Bool)
- @objc optional func sideMenuDidDisappear(menu: SideMenuNavigationController, animated: Bool)
- }
- internal protocol SideMenuNavigationControllerTransitionDelegate: class {
- func sideMenuTransitionDidDismiss(menu: Menu)
- }
- public struct SideMenuSettings: Model, InitializableStruct {
- public var allowPushOfSameClassTwice: Bool = true
- public var alwaysAnimate: Bool = true
- public var animationOptions: UIView.AnimationOptions = .curveEaseInOut
- public var blurEffectStyle: UIBlurEffect.Style? = nil
- public var completeGestureDuration: Double = 0.35
- public var completionCurve: UIView.AnimationCurve = .easeIn
- public var dismissDuration: Double = 0.35
- public var dismissOnPresent: Bool = true
- public var dismissOnPush: Bool = true
- public var dismissOnRotation: Bool = true
- public var dismissWhenBackgrounded: Bool = true
- public var enableSwipeToDismissGesture: Bool = true
- public var enableTapToDismissGesture: Bool = true
- public var initialSpringVelocity: CGFloat = 1
- public var menuWidth: CGFloat = {
- let appScreenRect = UIApplication.shared.keyWindow?.bounds ?? UIWindow().bounds
- let minimumSize = min(appScreenRect.width, appScreenRect.height)
- return min(round(minimumSize * 0.75), 240)
- }()
- public var presentingViewControllerUserInteractionEnabled: Bool = false
- public var presentingViewControllerUseSnapshot: Bool = false
- public var presentDuration: Double = 0.35
- public var presentationStyle: SideMenuPresentationStyle = .viewSlideOut
- public var pushStyle: SideMenuPushStyle = .default
- public var statusBarEndAlpha: CGFloat = 0
- public var usingSpringWithDamping: CGFloat = 1
- public init() {}
- }
- internal typealias Menu = SideMenuNavigationController
- typealias Model = MenuModel & PresentationModel & AnimationModel
- @objcMembers
- open class SideMenuNavigationController: UINavigationController {
-
- private lazy var _leftSide = Protected(false) { [weak self] oldValue, newValue in
- guard self?.isHidden != false else {
- Print.warning(.property, arguments: .leftSide, required: true)
- return oldValue
- }
- return newValue
- }
- private weak var _sideMenuManager: SideMenuManager?
- private weak var foundViewController: UIViewController?
- private var originalBackgroundColor: UIColor?
- private var rotating: Bool = false
- private var transitionController: SideMenuTransitionController?
- private var transitionInteractive: Bool = false
-
- public weak var sideMenuDelegate: SideMenuNavigationControllerDelegate?
-
- open private(set) weak var swipeToDismissGesture: UIPanGestureRecognizer? = nil
-
- open private(set) weak var tapToDismissGesture: UITapGestureRecognizer? = nil
- open var sideMenuManager: SideMenuManager {
- get { return _sideMenuManager ?? SideMenuManager.default }
- set {
- newValue.setMenu(self, forLeftSide: leftSide)
- if let sideMenuManager = _sideMenuManager, sideMenuManager !== newValue {
- let side = SideMenuManager.PresentDirection(leftSide: leftSide)
- Print.warning(.menuAlreadyAssigned, arguments: String(describing: self.self), side.name, String(describing: newValue))
- }
- _sideMenuManager = newValue
- }
- }
-
- open var settings = SideMenuSettings() {
- didSet {
- setupBlur()
- if !enableSwipeToDismissGesture {
- swipeToDismissGesture?.remove()
- }
- if !enableTapToDismissGesture {
- tapToDismissGesture?.remove()
- }
- }
- }
- public override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) {
- super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
- setup()
- }
- public init(rootViewController: UIViewController, settings: SideMenuSettings = SideMenuSettings()) {
- self.settings = settings
- super.init(rootViewController: rootViewController)
- setup()
- }
- required public init?(coder aDecoder: NSCoder) {
- super.init(coder: aDecoder)
- setup()
- }
-
- override open func awakeFromNib() {
- super.awakeFromNib()
- sideMenuManager.setMenu(self, forLeftSide: leftSide)
- }
-
- override open func viewWillAppear(_ animated: Bool) {
- super.viewWillAppear(animated)
- if topViewController == nil {
- Print.warning(.emptyMenu)
- }
-
- presentingViewController?.view.endEditing(true)
- foundViewController = nil
- activeDelegate?.sideMenuWillAppear?(menu: self, animated: animated)
- }
-
- override open func viewDidAppear(_ animated: Bool) {
- super.viewDidAppear(animated)
-
- if view.isHidden {
- dismiss(animated: false, completion: { [weak self] in
- self?.view.isHidden = false
- })
- } else {
- activeDelegate?.sideMenuDidAppear?(menu: self, animated: animated)
- }
- }
-
- override open func viewWillDisappear(_ animated: Bool) {
- super.viewWillDisappear(animated)
- defer { activeDelegate?.sideMenuWillDisappear?(menu: self, animated: animated) }
- guard !isBeingDismissed else { return }
-
-
- if let presentingView = presentingViewController?.view, let containerView = presentingView.superview {
- containerView.addSubview(view)
- }
- if dismissOnPresent {
-
- transitionController?.transition(presenting: false, animated: animated, alongsideTransition: { [weak self] in
- guard let self = self else { return }
- self.activeDelegate?.sideMenuWillDisappear?(menu: self, animated: animated)
- }, complete: false, completion: { [weak self] _ in
- guard let self = self else { return }
- self.activeDelegate?.sideMenuDidDisappear?(menu: self, animated: animated)
- self.view.isHidden = true
- })
- }
- }
- override open func viewDidDisappear(_ animated: Bool) {
- super.viewDidDisappear(animated)
-
-
-
-
- if isBeingDismissed {
- transitionController?.transition(presenting: false, animated: false)
- }
-
- if let tableViewController = topViewController as? UITableViewController,
- let tableView = tableViewController.tableView,
- let indexPaths = tableView.indexPathsForSelectedRows,
- tableViewController.clearsSelectionOnViewWillAppear {
- indexPaths.forEach { tableView.deselectRow(at: $0, animated: false) }
- }
- activeDelegate?.sideMenuDidDisappear?(menu: self, animated: animated)
- if isBeingDismissed {
- transitionController = nil
- } else if dismissOnPresent {
- view.isHidden = true
- }
- }
-
- override open func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
- super.viewWillTransition(to: size, with: coordinator)
-
-
- guard let transitionController = transitionController, !view.isHidden else { return }
- rotating = true
-
- let dismiss = self.presentingViewControllerUseSnapshot || self.dismissOnRotation
- coordinator.animate(alongsideTransition: { _ in
- if dismiss {
- transitionController.transition(presenting: false, animated: false, complete: false)
- } else {
- transitionController.layout()
- }
- }) { [weak self] _ in
- guard let self = self else { return }
- if dismiss {
- self.dismissMenu(animated: false)
- }
- self.rotating = false
- }
- }
- open override func viewWillLayoutSubviews() {
- super.viewWillLayoutSubviews()
- transitionController?.layout()
- }
-
- override open func pushViewController(_ viewController: UIViewController, animated: Bool) {
- guard viewControllers.count > 0 else {
-
-
- return super.pushViewController(viewController, animated: animated)
- }
- var alongsideTransition: (() -> Void)? = nil
- if dismissOnPush {
- alongsideTransition = { [weak self] in
- guard let self = self else { return }
- self.dismissAnimation(animated: animated || self.alwaysAnimate)
- }
- }
- let pushed = SideMenuPushCoordinator(config:
- .init(
- allowPushOfSameClassTwice: allowPushOfSameClassTwice,
- alongsideTransition: alongsideTransition,
- animated: animated,
- fromViewController: self,
- pushStyle: pushStyle,
- toViewController: viewController
- )
- ).start()
- if !pushed {
- super.pushViewController(viewController, animated: animated)
- }
- }
- override open var transitioningDelegate: UIViewControllerTransitioningDelegate? {
- get {
- guard transitionController == nil else { return transitionController }
- transitionController = SideMenuTransitionController(leftSide: leftSide, config: settings)
- transitionController?.delegate = self
- transitionController?.interactive = transitionInteractive
- transitionInteractive = false
- return transitionController
- }
- set { Print.warning(.transitioningDelegate, required: true) }
- }
- }
- extension SideMenuNavigationController: Model {
- @IBInspectable open var allowPushOfSameClassTwice: Bool {
- get { return settings.allowPushOfSameClassTwice }
- set { settings.allowPushOfSameClassTwice = newValue }
- }
- @IBInspectable open var alwaysAnimate: Bool {
- get { return settings.alwaysAnimate }
- set { settings.alwaysAnimate = newValue }
- }
- @IBInspectable open var animationOptions: UIView.AnimationOptions {
- get { return settings.animationOptions }
- set { settings.animationOptions = newValue }
- }
- open var blurEffectStyle: UIBlurEffect.Style? {
- get { return settings.blurEffectStyle }
- set { settings.blurEffectStyle = newValue }
- }
- @IBInspectable open var completeGestureDuration: Double {
- get { return settings.completeGestureDuration }
- set { settings.completeGestureDuration = newValue }
- }
- @IBInspectable open var completionCurve: UIView.AnimationCurve {
- get { return settings.completionCurve }
- set { settings.completionCurve = newValue }
- }
- @IBInspectable open var dismissDuration: Double {
- get { return settings.dismissDuration }
- set { settings.dismissDuration = newValue }
- }
- @IBInspectable open var dismissOnPresent: Bool {
- get { return settings.dismissOnPresent }
- set { settings.dismissOnPresent = newValue }
- }
- @IBInspectable open var dismissOnPush: Bool {
- get { return settings.dismissOnPush }
- set { settings.dismissOnPush = newValue }
- }
- @IBInspectable open var dismissOnRotation: Bool {
- get { return settings.dismissOnRotation }
- set { settings.dismissOnRotation = newValue }
- }
- @IBInspectable open var dismissWhenBackgrounded: Bool {
- get { return settings.dismissWhenBackgrounded }
- set { settings.dismissWhenBackgrounded = newValue }
- }
- @IBInspectable open var enableSwipeToDismissGesture: Bool {
- get { return settings.enableSwipeToDismissGesture }
- set { settings.enableSwipeToDismissGesture = newValue }
- }
- @IBInspectable open var enableTapToDismissGesture: Bool {
- get { return settings.enableTapToDismissGesture }
- set { settings.enableTapToDismissGesture = newValue }
- }
- @IBInspectable open var initialSpringVelocity: CGFloat {
- get { return settings.initialSpringVelocity }
- set { settings.initialSpringVelocity = newValue }
- }
-
- @IBInspectable open var leftSide: Bool {
- get { return _leftSide.value }
- set { _leftSide.value = newValue }
- }
-
-
- open override var isHidden: Bool {
- return super.isHidden
- }
- @IBInspectable open var menuWidth: CGFloat {
- get { return settings.menuWidth }
- set { settings.menuWidth = newValue }
- }
- @IBInspectable open var presentingViewControllerUserInteractionEnabled: Bool {
- get { return settings.presentingViewControllerUserInteractionEnabled }
- set { settings.presentingViewControllerUserInteractionEnabled = newValue }
- }
- @IBInspectable open var presentingViewControllerUseSnapshot: Bool {
- get { return settings.presentingViewControllerUseSnapshot }
- set { settings.presentingViewControllerUseSnapshot = newValue }
- }
- @IBInspectable open var presentDuration: Double {
- get { return settings.presentDuration }
- set { settings.presentDuration = newValue }
- }
- open var presentationStyle: SideMenuPresentationStyle {
- get { return settings.presentationStyle }
- set { settings.presentationStyle = newValue }
- }
- @IBInspectable open var pushStyle: SideMenuPushStyle {
- get { return settings.pushStyle }
- set { settings.pushStyle = newValue }
- }
- @IBInspectable open var statusBarEndAlpha: CGFloat {
- get { return settings.statusBarEndAlpha }
- set { settings.statusBarEndAlpha = newValue }
- }
- @IBInspectable open var usingSpringWithDamping: CGFloat {
- get { return settings.usingSpringWithDamping }
- set { settings.usingSpringWithDamping = newValue }
- }
- }
- extension SideMenuNavigationController: SideMenuTransitionControllerDelegate {
- func sideMenuTransitionController(_ transitionController: SideMenuTransitionController, didDismiss viewController: UIViewController) {
- sideMenuManager.sideMenuTransitionDidDismiss(menu: self)
- }
- func sideMenuTransitionController(_ transitionController: SideMenuTransitionController, didPresent viewController: UIViewController) {
- swipeToDismissGesture?.remove()
- swipeToDismissGesture = addSwipeToDismissGesture(to: view.superview)
- tapToDismissGesture = addTapToDismissGesture(to: view.superview)
- }
- }
- internal extension SideMenuNavigationController {
- func handleMenuPan(_ gesture: UIPanGestureRecognizer, _ presenting: Bool) {
- let width = menuWidth
- let distance = gesture.xTranslation / width
- let progress = max(min(distance * factor(presenting), 1), 0)
- switch (gesture.state) {
- case .began:
- if !presenting {
- dismissMenu(interactively: true)
- }
- fallthrough
- case .changed:
- transitionController?.handle(state: .update(progress: progress))
- case .ended:
- let velocity = gesture.xVelocity * factor(presenting)
- let finished = velocity >= 100 || velocity >= -50 && abs(progress) >= 0.5
- transitionController?.handle(state: finished ? .finish : .cancel)
- default:
- transitionController?.handle(state: .cancel)
- }
- }
- func cancelMenuPan(_ gesture: UIPanGestureRecognizer) {
- transitionController?.handle(state: .cancel)
- }
- func dismissMenu(animated flag: Bool = true, interactively: Bool = false, completion: (() -> Void)? = nil) {
- guard !isHidden else { return }
- transitionController?.interactive = interactively
- dismiss(animated: flag, completion: completion)
- }
-
- func present(from viewController: UIViewController?, interactively: Bool, completion: (() -> Void)? = nil) {
- guard let viewController = viewController else { return }
- transitionInteractive = interactively
- viewController.present(self, animated: true, completion: completion)
- }
- }
- private extension SideMenuNavigationController {
- weak var activeDelegate: SideMenuNavigationControllerDelegate? {
- guard !view.isHidden else { return nil }
- if let sideMenuDelegate = sideMenuDelegate { return sideMenuDelegate }
- return findViewController as? SideMenuNavigationControllerDelegate
- }
- var findViewController: UIViewController? {
- foundViewController = foundViewController ?? presentingViewController?.activeViewController
- return foundViewController
- }
- func dismissAnimation(animated: Bool) {
- transitionController?.transition(presenting: false, animated: animated, alongsideTransition: { [weak self] in
- guard let self = self else { return }
- self.activeDelegate?.sideMenuWillDisappear?(menu: self, animated: animated)
- }, completion: { [weak self] _ in
- guard let self = self else { return }
- self.activeDelegate?.sideMenuDidDisappear?(menu: self, animated: animated)
- self.dismiss(animated: false, completion: nil)
- self.foundViewController = nil
- })
- }
- func setup() {
- modalPresentationStyle = .overFullScreen
- setupBlur()
- if #available(iOS 13.0, *) {} else {
- registerForNotifications()
- }
- }
- func setupBlur() {
- removeBlur()
- guard let blurEffectStyle = blurEffectStyle,
- let view = topViewController?.view,
- !UIAccessibility.isReduceTransparencyEnabled else {
- return
- }
- originalBackgroundColor = originalBackgroundColor ?? view.backgroundColor
- let blurEffect = UIBlurEffect(style: blurEffectStyle)
- let blurView = UIVisualEffectView(effect: blurEffect)
- view.backgroundColor = UIColor.clear
- if let tableViewController = topViewController as? UITableViewController {
- tableViewController.tableView.backgroundView = blurView
- tableViewController.tableView.separatorEffect = UIVibrancyEffect(blurEffect: blurEffect)
- tableViewController.tableView.reloadData()
- } else {
- blurView.autoresizingMask = [.flexibleHeight, .flexibleWidth]
- blurView.frame = view.bounds
- view.insertSubview(blurView, at: 0)
- }
- }
- func removeBlur() {
- guard let originalBackgroundColor = originalBackgroundColor,
- let view = topViewController?.view else {
- return
- }
- self.originalBackgroundColor = nil
- view.backgroundColor = originalBackgroundColor
- if let tableViewController = topViewController as? UITableViewController {
- tableViewController.tableView.backgroundView = nil
- tableViewController.tableView.separatorEffect = nil
- tableViewController.tableView.reloadData()
- } else if let blurView = view.subviews.first as? UIVisualEffectView {
- blurView.removeFromSuperview()
- }
- }
- @available(iOS, deprecated: 13.0)
- func registerForNotifications() {
- NotificationCenter.default.removeObserver(self)
- [UIApplication.willChangeStatusBarFrameNotification,
- UIApplication.didEnterBackgroundNotification].forEach {
- NotificationCenter.default.addObserver(self, selector: #selector(handleNotification), name: $0, object: nil)
- }
- }
- @available(iOS, deprecated: 13.0)
- @objc func handleNotification(notification: NSNotification) {
- guard isHidden else { return }
- switch notification.name {
- case UIApplication.willChangeStatusBarFrameNotification:
-
- if !rotating {
- dismissMenu()
- }
- case UIApplication.didEnterBackgroundNotification:
- if dismissWhenBackgrounded {
- dismissMenu()
- }
- default: break
- }
- }
- @discardableResult func addSwipeToDismissGesture(to view: UIView?) -> UIPanGestureRecognizer? {
- guard enableSwipeToDismissGesture else { return nil }
- return UIPanGestureRecognizer(addTo: view, target: self, action: #selector(handleDismissMenuPan(_:)))?.with {
- $0.cancelsTouchesInView = false
- }
- }
- @discardableResult func addTapToDismissGesture(to view: UIView?) -> UITapGestureRecognizer? {
- guard enableTapToDismissGesture else { return nil }
- return UITapGestureRecognizer(addTo: view, target: self, action: #selector(handleDismissMenuTap(_:)))?.with {
- $0.cancelsTouchesInView = false
- }
- }
- @objc func handleDismissMenuTap(_ tap: UITapGestureRecognizer) {
- let hitTest = view.window?.hitTest(tap.location(in: view.superview), with: nil)
- guard hitTest == view.superview else { return }
- dismissMenu()
- }
- @objc func handleDismissMenuPan(_ gesture: UIPanGestureRecognizer) {
- handleMenuPan(gesture, false)
- }
- func factor(_ presenting: Bool) -> CGFloat {
- return presenting ? presentFactor : hideFactor
- }
- var presentFactor: CGFloat {
- return leftSide ? 1 : -1
- }
- var hideFactor: CGFloat {
- return -presentFactor
- }
- }
|