--- /dev/null
+//
+// SideMenuTransition.swift
+// Pods
+//
+// Created by Jon Kent on 1/14/16.
+//
+//
+
+import UIKit
+
+open class SideMenuTransition: UIPercentDrivenInteractiveTransition {
+
+ fileprivate var presenting = false
+ fileprivate var interactive = false
+ fileprivate weak var originalSuperview: UIView?
+ fileprivate weak var activeGesture: UIGestureRecognizer?
+ fileprivate var switchMenus = false {
+ didSet {
+ if switchMenus {
+ cancel()
+ }
+ }
+ }
+ fileprivate var menuWidth: CGFloat {
+ get {
+ let overriddenWidth = menuViewController?.menuWidth ?? 0
+ if overriddenWidth > CGFloat.ulpOfOne {
+ return overriddenWidth
+ }
+ return sideMenuManager.menuWidth
+ }
+ }
+ internal weak var sideMenuManager: SideMenuManager!
+ internal weak var mainViewController: UIViewController?
+ internal weak var menuViewController: UISideMenuNavigationController? {
+ get {
+ return presentDirection == .left ? sideMenuManager.menuLeftNavigationController : sideMenuManager.menuRightNavigationController
+ }
+ }
+ internal var presentDirection: UIRectEdge = .left
+ internal weak var tapView: UIView? {
+ didSet {
+ guard let tapView = tapView else {
+ return
+ }
+
+ tapView.autoresizingMask = [.flexibleHeight, .flexibleWidth]
+ let exitPanGesture = UIPanGestureRecognizer()
+ exitPanGesture.addTarget(self, action:#selector(SideMenuTransition.handleHideMenuPan(_:)))
+ let exitTapGesture = UITapGestureRecognizer()
+ exitTapGesture.addTarget(self, action: #selector(SideMenuTransition.handleHideMenuTap(_:)))
+ tapView.addGestureRecognizer(exitPanGesture)
+ tapView.addGestureRecognizer(exitTapGesture)
+ }
+ }
+ internal weak var statusBarView: UIView? {
+ didSet {
+ guard let statusBarView = statusBarView else {
+ return
+ }
+
+ statusBarView.backgroundColor = sideMenuManager.menuAnimationBackgroundColor ?? UIColor.black
+ statusBarView.isUserInteractionEnabled = false
+ }
+ }
+
+ required public init(sideMenuManager: SideMenuManager) {
+ super.init()
+
+ NotificationCenter.default.addObserver(self, selector:#selector(handleNotification), name: NSNotification.Name.UIApplicationDidEnterBackground, object: nil)
+ NotificationCenter.default.addObserver(self, selector:#selector(handleNotification), name: NSNotification.Name.UIApplicationWillChangeStatusBarFrame, object: nil)
+ self.sideMenuManager = sideMenuManager
+ }
+
+ deinit {
+ NotificationCenter.default.removeObserver(self)
+ }
+
+ fileprivate static var visibleViewController: UIViewController? {
+ get {
+ return getVisibleViewController(forViewController: UIApplication.shared.keyWindow?.rootViewController)
+ }
+ }
+
+ fileprivate class func getVisibleViewController(forViewController: UIViewController?) -> UIViewController? {
+ if let navigationController = forViewController as? UINavigationController {
+ return getVisibleViewController(forViewController: navigationController.visibleViewController)
+ }
+ if let tabBarController = forViewController as? UITabBarController {
+ return getVisibleViewController(forViewController: tabBarController.selectedViewController)
+ }
+ if let splitViewController = forViewController as? UISplitViewController {
+ return getVisibleViewController(forViewController: splitViewController.viewControllers.last)
+ }
+ if let presentedViewController = forViewController?.presentedViewController {
+ return getVisibleViewController(forViewController: presentedViewController)
+ }
+
+ return forViewController
+ }
+
+ @objc internal func handlePresentMenuLeftScreenEdge(_ edge: UIScreenEdgePanGestureRecognizer) {
+ presentDirection = .left
+ handlePresentMenuPan(edge)
+ }
+
+ @objc internal func handlePresentMenuRightScreenEdge(_ edge: UIScreenEdgePanGestureRecognizer) {
+ presentDirection = .right
+ handlePresentMenuPan(edge)
+ }
+
+ @objc internal func handlePresentMenuPan(_ pan: UIPanGestureRecognizer) {
+ if activeGesture == nil {
+ activeGesture = pan
+ } else if pan != activeGesture {
+ pan.isEnabled = false
+ pan.isEnabled = true
+ return
+ } else if pan.state != .began && pan.state != .changed {
+ activeGesture = nil
+ }
+
+ // how much distance have we panned in reference to the parent view?
+ guard let view = mainViewController?.view ?? pan.view else {
+ return
+ }
+
+ let transform = view.transform
+ view.transform = .identity
+ let translation = pan.translation(in: pan.view!)
+ view.transform = transform
+
+ // do some math to translate this to a percentage based value
+ if !interactive {
+ if translation.x == 0 {
+ return // not sure which way the user is swiping yet, so do nothing
+ }
+
+ if !(pan is UIScreenEdgePanGestureRecognizer) {
+ presentDirection = translation.x > 0 ? .left : .right
+ }
+
+ if let menuViewController = menuViewController, let visibleViewController = SideMenuTransition.visibleViewController {
+ interactive = true
+ visibleViewController.present(menuViewController, animated: true, completion: nil)
+ } else {
+ return
+ }
+ }
+
+ let direction: CGFloat = presentDirection == .left ? 1 : -1
+ let distance = translation.x / menuWidth
+ // now lets deal with different states that the gesture recognizer sends
+ switch (pan.state) {
+ case .began, .changed:
+ if pan is UIScreenEdgePanGestureRecognizer {
+ update(min(distance * direction, 1))
+ } else if distance > 0 && presentDirection == .right && sideMenuManager.menuLeftNavigationController != nil {
+ presentDirection = .left
+ switchMenus = true
+ } else if distance < 0 && presentDirection == .left && sideMenuManager.menuRightNavigationController != nil {
+ presentDirection = .right
+ switchMenus = true
+ } else {
+ update(min(distance * direction, 1))
+ }
+ default:
+ interactive = false
+ view.transform = .identity
+ let velocity = pan.velocity(in: pan.view!).x * direction
+ view.transform = transform
+ if velocity >= 100 || velocity >= -50 && abs(distance) >= 0.5 {
+ // bug workaround: animation briefly resets after call to finishInteractiveTransition() but before animateTransition completion is called.
+ if ProcessInfo().operatingSystemVersion.majorVersion == 8 && percentComplete > 1 - CGFloat.ulpOfOne {
+ update(0.9999)
+ }
+ finish()
+ } else {
+ cancel()
+ }
+ }
+ }
+
+ @objc internal func handleHideMenuPan(_ pan: UIPanGestureRecognizer) {
+ if activeGesture == nil {
+ activeGesture = pan
+ } else if pan != activeGesture {
+ pan.isEnabled = false
+ pan.isEnabled = true
+ return
+ }
+
+ let translation = pan.translation(in: pan.view!)
+ let direction:CGFloat = presentDirection == .left ? -1 : 1
+ let distance = translation.x / menuWidth * direction
+
+ switch (pan.state) {
+
+ case .began:
+ interactive = true
+ mainViewController?.dismiss(animated: true, completion: nil)
+ case .changed:
+ update(max(min(distance, 1), 0))
+ default:
+ interactive = false
+ let velocity = pan.velocity(in: pan.view!).x * direction
+ if velocity >= 100 || velocity >= -50 && distance >= 0.5 {
+ // bug workaround: animation briefly resets after call to finishInteractiveTransition() but before animateTransition completion is called.
+ if ProcessInfo().operatingSystemVersion.majorVersion == 8 && percentComplete > 1 - CGFloat.ulpOfOne {
+ update(0.9999)
+ }
+ finish()
+ activeGesture = nil
+ } else {
+ cancel()
+ activeGesture = nil
+ }
+ }
+ }
+
+ @objc internal func handleHideMenuTap(_ tap: UITapGestureRecognizer) {
+ menuViewController?.dismiss(animated: true, completion: nil)
+ }
+
+ @discardableResult internal func hideMenuStart() -> SideMenuTransition {
+ let menuView = menuViewController?.view
+ let mainView = mainViewController?.view
+
+ mainView?.transform = .identity
+ mainView?.alpha = 1
+ mainView?.frame.origin = .zero
+ menuView?.transform = .identity
+ menuView?.frame.origin.y = 0
+ menuView?.frame.size.width = menuWidth
+ menuView?.frame.size.height = mainView?.frame.height ?? 0 // in case status bar height changed
+ var statusBarFrame = UIApplication.shared.statusBarFrame
+ let statusBarOffset = SideMenuManager.appScreenRect.size.height - (mainView?.frame.maxY ?? 0)
+ // For in-call status bar, height is normally 40, which overlaps view. Instead, calculate height difference
+ // of view and set height to fill in remaining space.
+ if statusBarOffset >= CGFloat.ulpOfOne {
+ statusBarFrame.size.height = statusBarOffset
+ }
+ statusBarView?.frame = statusBarFrame
+ statusBarView?.alpha = 0
+
+ switch sideMenuManager.menuPresentMode {
+
+ case .viewSlideOut:
+ menuView?.alpha = 1 - sideMenuManager.menuAnimationFadeStrength
+ menuView?.frame.origin.x = presentDirection == .left ? 0 : (mainView?.frame.width ?? 0) - menuWidth
+ menuView?.transform = CGAffineTransform(scaleX: sideMenuManager.menuAnimationTransformScaleFactor, y: sideMenuManager.menuAnimationTransformScaleFactor)
+
+ case .viewSlideInOut:
+ menuView?.alpha = 1
+ menuView?.frame.origin.x = presentDirection == .left ? -menuView!.frame.width : mainView!.frame.width
+
+ case .menuSlideIn:
+ menuView?.alpha = 1
+ menuView?.frame.origin.x = presentDirection == .left ? -menuView!.frame.width : mainView!.frame.width
+
+ case .menuDissolveIn:
+ menuView?.alpha = 0
+ menuView?.frame.origin.x = presentDirection == .left ? 0 : mainView!.frame.width - menuWidth
+ }
+
+ return self
+ }
+
+ @discardableResult internal func hideMenuComplete() -> SideMenuTransition {
+ let menuView = menuViewController?.view
+ let mainView = mainViewController?.view
+
+ tapView?.removeFromSuperview()
+ statusBarView?.removeFromSuperview()
+ mainView?.motionEffects.removeAll()
+ mainView?.layer.shadowOpacity = 0
+ menuView?.layer.shadowOpacity = 0
+ if let topNavigationController = mainViewController as? UINavigationController {
+ topNavigationController.interactivePopGestureRecognizer!.isEnabled = true
+ }
+ if let originalSuperview = originalSuperview, let mainView = mainViewController?.view {
+ originalSuperview.addSubview(mainView)
+ let y = originalSuperview.bounds.height - mainView.frame.size.height
+ mainView.frame.origin.y = max(y, 0)
+ }
+
+ originalSuperview = nil
+ mainViewController = nil
+
+ return self
+ }
+
+ @discardableResult internal func presentMenuStart() -> SideMenuTransition {
+ let menuView = menuViewController?.view
+ let mainView = mainViewController?.view
+
+ menuView?.alpha = 1
+ menuView?.transform = .identity
+ menuView?.frame.size.width = menuWidth
+ let size = SideMenuManager.appScreenRect.size
+ menuView?.frame.origin.x = presentDirection == .left ? 0 : size.width - menuWidth
+ mainView?.transform = .identity
+ mainView?.frame.size.width = size.width
+ let statusBarOffset = size.height - (menuView?.bounds.height ?? 0)
+ mainView?.bounds.size.height = size.height - max(statusBarOffset, 0)
+ mainView?.frame.origin.y = 0
+ var statusBarFrame = UIApplication.shared.statusBarFrame
+ // For in-call status bar, height is normally 40, which overlaps view. Instead, calculate height difference
+ // of view and set height to fill in remaining space.
+ if statusBarOffset >= CGFloat.ulpOfOne {
+ statusBarFrame.size.height = statusBarOffset
+ }
+ tapView?.transform = .identity
+ tapView?.bounds = mainView!.bounds
+ statusBarView?.frame = statusBarFrame
+ statusBarView?.alpha = 1
+
+ switch sideMenuManager.menuPresentMode {
+
+ case .viewSlideOut, .viewSlideInOut:
+ mainView?.layer.shadowColor = sideMenuManager.menuShadowColor.cgColor
+ mainView?.layer.shadowRadius = sideMenuManager.menuShadowRadius
+ mainView?.layer.shadowOpacity = sideMenuManager.menuShadowOpacity
+ mainView?.layer.shadowOffset = CGSize(width: 0, height: 0)
+ let direction:CGFloat = presentDirection == .left ? 1 : -1
+ mainView?.frame.origin.x = direction * (menuView!.frame.width)
+
+ case .menuSlideIn, .menuDissolveIn:
+ if sideMenuManager.menuBlurEffectStyle == nil {
+ menuView?.layer.shadowColor = sideMenuManager.menuShadowColor.cgColor
+ menuView?.layer.shadowRadius = sideMenuManager.menuShadowRadius
+ menuView?.layer.shadowOpacity = sideMenuManager.menuShadowOpacity
+ menuView?.layer.shadowOffset = CGSize(width: 0, height: 0)
+ }
+ mainView?.frame.origin.x = 0
+ }
+
+ if sideMenuManager.menuPresentMode != .viewSlideOut {
+ mainView?.transform = CGAffineTransform(scaleX: sideMenuManager.menuAnimationTransformScaleFactor, y: sideMenuManager.menuAnimationTransformScaleFactor)
+ if sideMenuManager.menuAnimationTransformScaleFactor > 1 {
+ tapView?.transform = mainView!.transform
+ }
+ mainView?.alpha = 1 - sideMenuManager.menuAnimationFadeStrength
+ }
+
+ return self
+ }
+
+ @discardableResult internal func presentMenuComplete() -> SideMenuTransition {
+ switch sideMenuManager.menuPresentMode {
+ case .menuSlideIn, .menuDissolveIn, .viewSlideInOut:
+ if let mainView = mainViewController?.view, sideMenuManager.menuParallaxStrength != 0 {
+ let horizontal = UIInterpolatingMotionEffect(keyPath: "center.x", type: .tiltAlongHorizontalAxis)
+ horizontal.minimumRelativeValue = -sideMenuManager.menuParallaxStrength
+ horizontal.maximumRelativeValue = sideMenuManager.menuParallaxStrength
+
+ let vertical = UIInterpolatingMotionEffect(keyPath: "center.y", type: .tiltAlongVerticalAxis)
+ vertical.minimumRelativeValue = -sideMenuManager.menuParallaxStrength
+ vertical.maximumRelativeValue = sideMenuManager.menuParallaxStrength
+
+ let group = UIMotionEffectGroup()
+ group.motionEffects = [horizontal, vertical]
+ mainView.addMotionEffect(group)
+ }
+ case .viewSlideOut: break;
+ }
+ if let topNavigationController = mainViewController as? UINavigationController {
+ topNavigationController.interactivePopGestureRecognizer!.isEnabled = false
+ }
+
+ return self
+ }
+
+ @objc internal func handleNotification(notification: NSNotification) {
+ guard menuViewController?.presentedViewController == nil &&
+ menuViewController?.presentingViewController != nil else {
+ return
+ }
+
+ if let originalSuperview = originalSuperview, let mainViewController = mainViewController {
+ originalSuperview.addSubview(mainViewController.view)
+ }
+
+ if notification.name == NSNotification.Name.UIApplicationDidEnterBackground {
+ hideMenuStart().hideMenuComplete()
+ menuViewController?.dismiss(animated: false, completion: nil)
+ return
+ }
+
+ UIView.animate(withDuration: sideMenuManager.menuAnimationDismissDuration,
+ delay: 0,
+ usingSpringWithDamping: sideMenuManager.menuAnimationUsingSpringWithDamping,
+ initialSpringVelocity: sideMenuManager.menuAnimationInitialSpringVelocity,
+ options: sideMenuManager.menuAnimationOptions,
+ animations: {
+ self.hideMenuStart()
+ }) { (finished) -> Void in
+ self.hideMenuComplete()
+ self.menuViewController?.dismiss(animated: false, completion: nil)
+ }
+ }
+
+}
+
+extension SideMenuTransition: UIViewControllerAnimatedTransitioning {
+
+ // animate a change from one viewcontroller to another
+ open func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
+
+ // get reference to our fromView, toView and the container view that we should perform the transition in
+ let container = transitionContext.containerView
+ // prevent any other menu gestures from firing
+ container.isUserInteractionEnabled = false
+
+ if let menuBackgroundColor = sideMenuManager.menuAnimationBackgroundColor {
+ container.backgroundColor = menuBackgroundColor
+ }
+
+ let fromViewController = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.from)!
+ let toViewController = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.to)!
+
+ // assign references to our menu view controller and the 'bottom' view controller from the tuple
+ // remember that our menuViewController will alternate between the from and to view controller depending if we're presenting or dismissing
+ mainViewController = presenting ? fromViewController : toViewController
+
+ let menuView = menuViewController!.view!
+ let topView = mainViewController!.view!
+
+ // prepare menu items to slide in
+ if presenting {
+ originalSuperview = topView.superview
+
+ // add the both views to our view controller
+ switch sideMenuManager.menuPresentMode {
+ case .viewSlideOut, .viewSlideInOut:
+ container.addSubview(menuView)
+ container.addSubview(topView)
+ case .menuSlideIn, .menuDissolveIn:
+ container.addSubview(topView)
+ container.addSubview(menuView)
+ }
+
+ if sideMenuManager.menuFadeStatusBar {
+ let statusBarView = UIView()
+ self.statusBarView = statusBarView
+ container.addSubview(statusBarView)
+ }
+
+ hideMenuStart()
+ }
+
+ let animate = {
+ if self.presenting {
+ self.presentMenuStart()
+ } else {
+ self.hideMenuStart()
+ }
+ }
+
+ let complete = {
+ container.isUserInteractionEnabled = true
+
+ // tell our transitionContext object that we've finished animating
+ if transitionContext.transitionWasCancelled {
+ let viewControllerForPresentedMenu = self.mainViewController
+
+ if self.presenting {
+ self.hideMenuComplete()
+ } else {
+ self.presentMenuComplete()
+ }
+
+ transitionContext.completeTransition(false)
+
+ if self.switchMenus {
+ self.switchMenus = false
+ viewControllerForPresentedMenu?.present(self.menuViewController!, animated: true, completion: nil)
+ }
+
+ return
+ }
+
+ if self.presenting {
+ self.presentMenuComplete()
+ transitionContext.completeTransition(true)
+ switch self.sideMenuManager.menuPresentMode {
+ case .viewSlideOut, .viewSlideInOut:
+ container.addSubview(topView)
+ case .menuSlideIn, .menuDissolveIn:
+ container.insertSubview(topView, at: 0)
+ }
+ if !self.sideMenuManager.menuPresentingViewControllerUserInteractionEnabled {
+ let tapView = UIView()
+ container.insertSubview(tapView, aboveSubview: topView)
+ tapView.bounds = container.bounds
+ tapView.center = topView.center
+ if self.sideMenuManager.menuAnimationTransformScaleFactor > 1 {
+ tapView.transform = topView.transform
+ }
+ self.tapView = tapView
+ }
+ if let statusBarView = self.statusBarView {
+ container.bringSubview(toFront: statusBarView)
+ }
+
+ return
+ }
+
+ self.hideMenuComplete()
+ transitionContext.completeTransition(true)
+ menuView.removeFromSuperview()
+ }
+
+ // perform the animation!
+ let duration = transitionDuration(using: transitionContext)
+ if interactive {
+ UIView.animate(withDuration: duration,
+ delay: duration, // HACK: If zero, the animation briefly flashes in iOS 11. UIViewPropertyAnimators (iOS 10+) may resolve this.
+ options: .curveLinear,
+ animations: {
+ animate()
+ }, completion: { (finished) in
+ complete()
+ })
+ } else {
+ UIView.animate(withDuration: duration,
+ delay: 0,
+ usingSpringWithDamping: sideMenuManager.menuAnimationUsingSpringWithDamping,
+ initialSpringVelocity: sideMenuManager.menuAnimationInitialSpringVelocity,
+ options: sideMenuManager.menuAnimationOptions,
+ animations: {
+ animate()
+ }) { (finished) -> Void in
+ complete()
+ }
+ }
+ }
+
+ // return how many seconds the transiton animation will take
+ open func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
+ if interactive {
+ return sideMenuManager.menuAnimationCompleteGestureDuration
+ }
+ return presenting ? sideMenuManager.menuAnimationPresentDuration : sideMenuManager.menuAnimationDismissDuration
+ }
+
+ open override func update(_ percentComplete: CGFloat) {
+ guard !switchMenus else {
+ return
+ }
+
+ super.update(percentComplete)
+ }
+
+}
+
+extension SideMenuTransition: UIViewControllerTransitioningDelegate {
+
+ // return the animator when presenting a viewcontroller
+ // rememeber that an animator (or animation controller) is any object that aheres to the UIViewControllerAnimatedTransitioning protocol
+ open func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
+ self.presenting = true
+ presentDirection = presented == sideMenuManager.menuLeftNavigationController ? .left : .right
+ return self
+ }
+
+ // return the animator used when dismissing from a viewcontroller
+ open func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
+ presenting = false
+ return self
+ }
+
+ open func interactionControllerForPresentation(using animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {
+ // if our interactive flag is true, return the transition manager object
+ // otherwise return nil
+ return interactive ? self : nil
+ }
+
+ open func interactionControllerForDismissal(using animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {
+ return interactive ? self : nil
+ }
+
+}