693 lines
28 KiB
Swift
693 lines
28 KiB
Swift
//
|
|
// SideMenuNavigationController.swift
|
|
//
|
|
// Created by Jon Kent on 1/14/16.
|
|
// Copyright © 2016 Jon Kent. All rights reserved.
|
|
//
|
|
|
|
import UIKit
|
|
|
|
@objc public enum SideMenuPushStyle: Int { case
|
|
`default`,
|
|
popWhenPossible,
|
|
preserve,
|
|
preserveAndHideBackButton,
|
|
replace,
|
|
subMenu
|
|
}
|
|
|
|
@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: MenuModel, 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 = 1
|
|
public var usingSpringWithDamping: CGFloat = 1
|
|
|
|
public init() {}
|
|
}
|
|
|
|
internal typealias Menu = SideMenuNavigationController
|
|
|
|
@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 weak var interactionController: SideMenuInteractionController?
|
|
private var interactive: Bool = false
|
|
private var originalBackgroundColor: UIColor?
|
|
private var rotating: Bool = false
|
|
private var transitionController: SideMenuTransitionController?
|
|
|
|
/// Delegate for receiving appear and disappear related events. If `nil` the visible view controller that displays a `SideMenuNavigationController` automatically receives these events.
|
|
public weak var sideMenuDelegate: SideMenuNavigationControllerDelegate?
|
|
|
|
/// The swipe to dismiss gesture.
|
|
open private(set) weak var swipeToDismissGesture: UIPanGestureRecognizer? = nil
|
|
/// The tap to dismiss gesture.
|
|
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
|
|
}
|
|
}
|
|
|
|
/// The menu settings.
|
|
open var settings = SideMenuSettings() {
|
|
didSet {
|
|
setupBlur()
|
|
if !enableSwipeToDismissGesture {
|
|
removeSwipeGesture()
|
|
}
|
|
if !enableTapToDismissGesture {
|
|
removeTapGesture()
|
|
}
|
|
}
|
|
}
|
|
|
|
public override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) {
|
|
super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
|
|
setup()
|
|
}
|
|
|
|
public init(rootViewController: UIViewController, settings: SideMenuSettings = SideMenuSettings()) {
|
|
super.init(rootViewController: rootViewController)
|
|
self.settings = settings
|
|
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)
|
|
}
|
|
|
|
// Dismiss keyboard to prevent weird keyboard animations from occurring during transition
|
|
presentingViewController?.view.endEditing(true)
|
|
|
|
foundViewController = 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 {
|
|
transitionController?.transition(presenting: false, animated: false)
|
|
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 }
|
|
|
|
// 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 let presentingView = presentingViewController?.view, let containerView = presentingView.superview {
|
|
containerView.addSubview(view)
|
|
}
|
|
|
|
if dismissOnPresent {
|
|
// 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.
|
|
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)
|
|
|
|
// 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 foundViewController = self.visibleViewController(from: presentingViewController), foundViewController.view.window == nil {
|
|
transitionController?.transition(presenting: false, animated: false)
|
|
}
|
|
|
|
// Clear selecton on UITableViewControllers when reappearing using custom transitions
|
|
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)
|
|
|
|
// Don't bother resizing if the view isn't visible
|
|
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 && pushStyle != .subMenu else {
|
|
// NOTE: pushViewController is called by init(rootViewController: UIViewController)
|
|
// so we must perform the normal super method in this case
|
|
return super.pushViewController(viewController, animated: animated)
|
|
}
|
|
|
|
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 {
|
|
return Print.warning(.cannotPush, arguments: String(describing: potentialNavigationController.self), required: true)
|
|
}
|
|
|
|
// To avoid overlapping dismiss & pop/push calls, create a transaction block where the menu
|
|
// is dismissed after showing the appropriate screen
|
|
CATransaction.begin()
|
|
defer { CATransaction.commit() }
|
|
|
|
if dismissOnPush {
|
|
let animated = animated || alwaysAnimate
|
|
if animated {
|
|
let areAnimationsEnabled = UIView.areAnimationsEnabled
|
|
UIView.setAnimationsEnabled(true)
|
|
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
|
|
})
|
|
UIView.setAnimationsEnabled(areAnimationsEnabled)
|
|
}
|
|
}
|
|
|
|
if let lastViewController = navigationController.viewControllers.last,
|
|
!allowPushOfSameClassTwice && type(of: lastViewController) == type(of: viewController) {
|
|
return
|
|
}
|
|
|
|
switch pushStyle {
|
|
case .subMenu: return // handled earlier
|
|
case .default: break
|
|
case .popWhenPossible:
|
|
for subViewController in navigationController.viewControllers.reversed() {
|
|
if type(of: subViewController) == type(of: viewController) {
|
|
navigationController.popToViewController(subViewController, animated: animated)
|
|
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 pushStyle == .preserveAndHideBackButton {
|
|
preservedViewController.navigationItem.hidesBackButton = true
|
|
}
|
|
viewControllers.append(preservedViewController)
|
|
return navigationController.setViewControllers(viewControllers, animated: animated)
|
|
}
|
|
if pushStyle == .preserveAndHideBackButton {
|
|
viewController.navigationItem.hidesBackButton = true
|
|
}
|
|
case .replace:
|
|
viewController.navigationItem.hidesBackButton = true
|
|
return navigationController.setViewControllers([viewController], animated: animated)
|
|
}
|
|
|
|
navigationController.pushViewController(viewController, animated: animated)
|
|
}
|
|
|
|
override open var transitioningDelegate: UIViewControllerTransitioningDelegate? {
|
|
get { return self }
|
|
set { Print.warning(.transitioningDelegate, required: true) }
|
|
}
|
|
}
|
|
|
|
// Interface
|
|
extension SideMenuNavigationController: MenuModel {
|
|
|
|
@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 }
|
|
}
|
|
|
|
/// 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 {
|
|
get { return _leftSide.value }
|
|
set { _leftSide.value = newValue }
|
|
}
|
|
|
|
/// Indicates if the menu is anywhere in the view hierarchy, even if covered by another view controller.
|
|
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 }
|
|
}
|
|
}
|
|
|
|
// IMPORTANT: These methods must be declared open or they will not be called.
|
|
extension SideMenuNavigationController: UIViewControllerTransitioningDelegate {
|
|
|
|
open func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
|
|
transitionController = SideMenuTransitionController(
|
|
config: self,
|
|
leftSide: leftSide,
|
|
delegate: self)
|
|
return transitionController
|
|
}
|
|
|
|
open func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
|
|
return transitionController
|
|
}
|
|
|
|
open func interactionControllerForPresentation(using animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {
|
|
return interactionController(using: animator)
|
|
}
|
|
|
|
open func interactionControllerForDismissal(using animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {
|
|
return interactionController(using: animator)
|
|
}
|
|
|
|
private func interactionController(using animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {
|
|
guard interactive else { return nil }
|
|
interactive = false
|
|
let interactionController = SideMenuInteractionController(cancelWhenBackgrounded: dismissWhenBackgrounded, completionCurve: completionCurve)
|
|
self.interactionController = interactionController
|
|
return interactionController
|
|
}
|
|
}
|
|
|
|
extension SideMenuNavigationController: SideMenuTransitionControllerDelegate {
|
|
|
|
internal func sideMenuTransitionController(_ transitionController: SideMenuTransitionController, didDismiss viewController: UIViewController) {
|
|
sideMenuManager.sideMenuTransitionDidDismiss(menu: self)
|
|
}
|
|
|
|
internal func sideMenuTransitionController(_ transitionController: SideMenuTransitionController, didPresent viewController: UIViewController) {
|
|
removeSwipeGesture()
|
|
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)
|
|
}
|
|
interactionController?.handle(state: .update(progress: progress))
|
|
case .changed:
|
|
interactionController?.handle(state: .update(progress: progress))
|
|
case .ended:
|
|
let velocity = gesture.xVelocity * factor(presenting)
|
|
let finished = velocity >= 100 || velocity >= -50 && abs(progress) >= 0.5
|
|
interactionController?.handle(state: finished ? .finish : .cancel)
|
|
default:
|
|
interactionController?.handle(state: .cancel)
|
|
}
|
|
}
|
|
|
|
func cancelMenuPan(_ gesture: UIPanGestureRecognizer) {
|
|
interactionController?.handle(state: .cancel)
|
|
}
|
|
|
|
func dismissMenu(animated flag: Bool = true, interactively interactive: Bool = false, completion: (() -> Void)? = nil) {
|
|
guard !isHidden else { return }
|
|
self.interactive = interactive
|
|
dismiss(animated: flag, completion: completion)
|
|
}
|
|
|
|
// Note: although this method is syntactically reversed it allows the interactive property to scoped privately
|
|
func presentFrom(_ viewControllerToPresentFrom: UIViewController?, interactively interactive: Bool, completion: (() -> Void)? = nil) {
|
|
guard let viewControllerToPresentFrom = viewControllerToPresentFrom else { return }
|
|
self.interactive = interactive
|
|
viewControllerToPresentFrom.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 visibleViewController(from: presentingViewController) as? SideMenuNavigationControllerDelegate
|
|
}
|
|
|
|
func visibleViewController(from: UIViewController?) -> UIViewController? {
|
|
if let foundDelegate = foundViewController {
|
|
return foundDelegate
|
|
}
|
|
if let navigationController = from as? UINavigationController {
|
|
return visibleViewController(from: navigationController.topViewController)
|
|
}
|
|
if let tabBarController = from as? UITabBarController {
|
|
return visibleViewController(from: tabBarController.selectedViewController)
|
|
}
|
|
if let splitViewController = from as? UISplitViewController {
|
|
return visibleViewController(from: splitViewController.viewControllers.last)
|
|
}
|
|
|
|
foundViewController = from
|
|
return from
|
|
}
|
|
|
|
func setup() {
|
|
modalPresentationStyle = .overFullScreen
|
|
|
|
setupBlur()
|
|
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()
|
|
}
|
|
}
|
|
|
|
func removeSwipeGesture() {
|
|
if let swipeToDismissGesture = swipeToDismissGesture {
|
|
swipeToDismissGesture.view?.removeGestureRecognizer(swipeToDismissGesture)
|
|
}
|
|
}
|
|
|
|
func removeTapGesture() {
|
|
if let tapToDismissGesture = tapToDismissGesture {
|
|
tapToDismissGesture.view?.removeGestureRecognizer(tapToDismissGesture)
|
|
}
|
|
}
|
|
|
|
func registerForNotifications() {
|
|
NotificationCenter.default.removeObserver(self)
|
|
|
|
[UIApplication.willChangeStatusBarFrameNotification,
|
|
UIApplication.didEnterBackgroundNotification].forEach {
|
|
NotificationCenter.default.addObserver(self, selector: #selector(handleNotification), name: $0, object: nil)
|
|
}
|
|
}
|
|
|
|
@objc func handleNotification(notification: NSNotification) {
|
|
guard isHidden else { return }
|
|
|
|
switch notification.name {
|
|
case UIApplication.willChangeStatusBarFrameNotification:
|
|
// Dismiss for in-call status bar changes but not rotation
|
|
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
|
|
}
|
|
}
|