Permalink
1087 lines (875 sloc) 41.5 KB
//
// SlideMenuController.swift
//
// Created by Yuji Hato on 12/3/14.
//
import Foundation
import UIKit
@objc public protocol SlideMenuControllerDelegate {
@objc optional func leftWillOpen()
@objc optional func leftDidOpen()
@objc optional func leftWillClose()
@objc optional func leftDidClose()
@objc optional func rightWillOpen()
@objc optional func rightDidOpen()
@objc optional func rightWillClose()
@objc optional func rightDidClose()
}
public struct SlideMenuOptions {
public static var leftViewWidth: CGFloat = 270.0
public static var leftBezelWidth: CGFloat? = 16.0
public static var contentViewScale: CGFloat = 0.96
public static var contentViewOpacity: CGFloat = 0.5
public static var contentViewDrag: Bool = false
public static var shadowOpacity: CGFloat = 0.0
public static var shadowRadius: CGFloat = 0.0
public static var shadowOffset: CGSize = CGSize(width: 0,height: 0)
public static var panFromBezel: Bool = true
public static var animationDuration: CGFloat = 0.4
public static var animationOptions: UIViewAnimationOptions = []
public static var rightViewWidth: CGFloat = 270.0
public static var rightBezelWidth: CGFloat? = 16.0
public static var rightPanFromBezel: Bool = true
public static var hideStatusBar: Bool = true
public static var pointOfNoReturnWidth: CGFloat = 44.0
public static var simultaneousGestureRecognizers: Bool = true
public static var opacityViewBackgroundColor: UIColor = UIColor.black
public static var panGesturesEnabled: Bool = true
public static var tapGesturesEnabled: Bool = true
}
open class SlideMenuController: UIViewController, UIGestureRecognizerDelegate {
public enum SlideAction {
case open
case close
}
public enum TrackAction {
case leftTapOpen
case leftTapClose
case leftFlickOpen
case leftFlickClose
case rightTapOpen
case rightTapClose
case rightFlickOpen
case rightFlickClose
}
struct PanInfo {
var action: SlideAction
var shouldBounce: Bool
var velocity: CGFloat
}
open weak var delegate: SlideMenuControllerDelegate?
open var opacityView = UIView()
open var mainContainerView = UIView()
open var leftContainerView = UIView()
open var rightContainerView = UIView()
open var mainViewController: UIViewController?
open var leftViewController: UIViewController?
open var leftPanGesture: UIPanGestureRecognizer?
open var leftTapGesture: UITapGestureRecognizer?
open var rightViewController: UIViewController?
open var rightPanGesture: UIPanGestureRecognizer?
open var rightTapGesture: UITapGestureRecognizer?
public required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
public override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) {
super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
}
public convenience init(mainViewController: UIViewController, leftMenuViewController: UIViewController) {
self.init()
self.mainViewController = mainViewController
leftViewController = leftMenuViewController
initView()
}
public convenience init(mainViewController: UIViewController, rightMenuViewController: UIViewController) {
self.init()
self.mainViewController = mainViewController
rightViewController = rightMenuViewController
initView()
}
public convenience init(mainViewController: UIViewController, leftMenuViewController: UIViewController, rightMenuViewController: UIViewController) {
self.init()
self.mainViewController = mainViewController
leftViewController = leftMenuViewController
rightViewController = rightMenuViewController
initView()
}
open override func awakeFromNib() {
initView()
}
deinit { }
open func initView() {
mainContainerView = UIView(frame: view.bounds)
mainContainerView.backgroundColor = UIColor.clear
mainContainerView.autoresizingMask = [.flexibleHeight, .flexibleWidth]
view.insertSubview(mainContainerView, at: 0)
var opacityframe: CGRect = view.bounds
let opacityOffset: CGFloat = 0
opacityframe.origin.y = opacityframe.origin.y + opacityOffset
opacityframe.size.height = opacityframe.size.height - opacityOffset
opacityView = UIView(frame: opacityframe)
opacityView.backgroundColor = SlideMenuOptions.opacityViewBackgroundColor
opacityView.autoresizingMask = [UIViewAutoresizing.flexibleHeight, UIViewAutoresizing.flexibleWidth]
opacityView.layer.opacity = 0.0
view.insertSubview(opacityView, at: 1)
if leftViewController != nil {
var leftFrame: CGRect = view.bounds
leftFrame.size.width = SlideMenuOptions.leftViewWidth
leftFrame.origin.x = leftMinOrigin()
let leftOffset: CGFloat = 0
leftFrame.origin.y = leftFrame.origin.y + leftOffset
leftFrame.size.height = leftFrame.size.height - leftOffset
leftContainerView = UIView(frame: leftFrame)
leftContainerView.backgroundColor = UIColor.clear
leftContainerView.autoresizingMask = UIViewAutoresizing.flexibleHeight
view.insertSubview(leftContainerView, at: 2)
addLeftGestures()
}
if rightViewController != nil {
var rightFrame: CGRect = view.bounds
rightFrame.size.width = SlideMenuOptions.rightViewWidth
rightFrame.origin.x = rightMinOrigin()
let rightOffset: CGFloat = 0
rightFrame.origin.y = rightFrame.origin.y + rightOffset
rightFrame.size.height = rightFrame.size.height - rightOffset
rightContainerView = UIView(frame: rightFrame)
rightContainerView.backgroundColor = UIColor.clear
rightContainerView.autoresizingMask = UIViewAutoresizing.flexibleHeight
view.insertSubview(rightContainerView, at: 3)
addRightGestures()
}
}
open override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransition(to: size, with: coordinator)
mainContainerView.transform = CGAffineTransform(scaleX: 1.0, y: 1.0)
leftContainerView.isHidden = true
rightContainerView.isHidden = true
coordinator.animate(alongsideTransition: nil, completion: { (context: UIViewControllerTransitionCoordinatorContext!) -> Void in
self.closeLeftNonAnimation()
self.closeRightNonAnimation()
self.leftContainerView.isHidden = false
self.rightContainerView.isHidden = false
if self.leftPanGesture != nil && self.leftPanGesture != nil {
self.removeLeftGestures()
self.addLeftGestures()
}
if self.rightPanGesture != nil && self.rightPanGesture != nil {
self.removeRightGestures()
self.addRightGestures()
}
})
}
open override func viewDidLoad() {
super.viewDidLoad()
edgesForExtendedLayout = UIRectEdge()
}
override open func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
//automatically called
//self.mainViewController?.viewWillAppear(animated)
}
open override var supportedInterfaceOrientations : UIInterfaceOrientationMask {
if let mainController = self.mainViewController{
return mainController.supportedInterfaceOrientations
}
return UIInterfaceOrientationMask.all
}
open override var shouldAutorotate : Bool {
return mainViewController?.shouldAutorotate ?? false
}
open override func viewWillLayoutSubviews() {
// topLayoutGuideの値が確定するこのタイミングで各種ViewControllerをセットする
setUpViewController(mainContainerView, targetViewController: mainViewController)
setUpViewController(leftContainerView, targetViewController: leftViewController)
setUpViewController(rightContainerView, targetViewController: rightViewController)
}
open override var preferredStatusBarStyle: UIStatusBarStyle {
return self.mainViewController?.preferredStatusBarStyle ?? .default
}
open override func openLeft() {
guard let _ = leftViewController else { // If leftViewController is nil, then return
return
}
self.delegate?.leftWillOpen?()
setOpenWindowLevel()
// for call viewWillAppear of leftViewController
leftViewController?.beginAppearanceTransition(isLeftHidden(), animated: true)
openLeftWithVelocity(0.0)
track(.leftTapOpen)
}
open override func openRight() {
guard let _ = rightViewController else { // If rightViewController is nil, then return
return
}
self.delegate?.rightWillOpen?()
setOpenWindowLevel()
rightViewController?.beginAppearanceTransition(isRightHidden(), animated: true)
openRightWithVelocity(0.0)
track(.rightTapOpen)
}
open override func closeLeft() {
guard let _ = leftViewController else { // If leftViewController is nil, then return
return
}
self.delegate?.leftWillClose?()
leftViewController?.beginAppearanceTransition(isLeftHidden(), animated: true)
closeLeftWithVelocity(0.0)
setCloseWindowLevel()
}
open override func closeRight() {
guard let _ = rightViewController else { // If rightViewController is nil, then return
return
}
self.delegate?.rightWillClose?()
rightViewController?.beginAppearanceTransition(isRightHidden(), animated: true)
closeRightWithVelocity(0.0)
setCloseWindowLevel()
}
open func addLeftGestures() {
if leftViewController != nil {
if SlideMenuOptions.panGesturesEnabled {
if leftPanGesture == nil {
leftPanGesture = UIPanGestureRecognizer(target: self, action: #selector(self.handleLeftPanGesture(_:)))
leftPanGesture!.delegate = self
view.addGestureRecognizer(leftPanGesture!)
}
}
if SlideMenuOptions.tapGesturesEnabled {
if leftTapGesture == nil {
leftTapGesture = UITapGestureRecognizer(target: self, action: #selector(self.toggleLeft))
leftTapGesture!.delegate = self
view.addGestureRecognizer(leftTapGesture!)
}
}
}
}
open func addRightGestures() {
if rightViewController != nil {
if SlideMenuOptions.panGesturesEnabled {
if rightPanGesture == nil {
rightPanGesture = UIPanGestureRecognizer(target: self, action: #selector(self.handleRightPanGesture(_:)))
rightPanGesture!.delegate = self
view.addGestureRecognizer(rightPanGesture!)
}
}
if SlideMenuOptions.tapGesturesEnabled {
if rightTapGesture == nil {
rightTapGesture = UITapGestureRecognizer(target: self, action: #selector(self.toggleRight))
rightTapGesture!.delegate = self
view.addGestureRecognizer(rightTapGesture!)
}
}
}
}
open func removeLeftGestures() {
if leftPanGesture != nil {
view.removeGestureRecognizer(leftPanGesture!)
leftPanGesture = nil
}
if leftTapGesture != nil {
view.removeGestureRecognizer(leftTapGesture!)
leftTapGesture = nil
}
}
open func removeRightGestures() {
if rightPanGesture != nil {
view.removeGestureRecognizer(rightPanGesture!)
rightPanGesture = nil
}
if rightTapGesture != nil {
view.removeGestureRecognizer(rightTapGesture!)
rightTapGesture = nil
}
}
open func isTagetViewController() -> Bool {
// Function to determine the target ViewController
// Please to override it if necessary
return true
}
open func track(_ trackAction: TrackAction) {
// function is for tracking
// Please to override it if necessary
}
struct LeftPanState {
static var frameAtStartOfPan: CGRect = CGRect.zero
static var startPointOfPan: CGPoint = CGPoint.zero
static var wasOpenAtStartOfPan: Bool = false
static var wasHiddenAtStartOfPan: Bool = false
static var lastState : UIGestureRecognizerState = .ended
}
@objc func handleLeftPanGesture(_ panGesture: UIPanGestureRecognizer) {
if !isTagetViewController() {
return
}
if isRightOpen() {
return
}
switch panGesture.state {
case UIGestureRecognizerState.began:
if LeftPanState.lastState != .ended && LeftPanState.lastState != .cancelled && LeftPanState.lastState != .failed {
return
}
if isLeftHidden() {
self.delegate?.leftWillOpen?()
} else {
self.delegate?.leftWillClose?()
}
LeftPanState.frameAtStartOfPan = leftContainerView.frame
LeftPanState.startPointOfPan = panGesture.location(in: view)
LeftPanState.wasOpenAtStartOfPan = isLeftOpen()
LeftPanState.wasHiddenAtStartOfPan = isLeftHidden()
leftViewController?.beginAppearanceTransition(LeftPanState.wasHiddenAtStartOfPan, animated: true)
addShadowToView(leftContainerView)
setOpenWindowLevel()
case UIGestureRecognizerState.changed:
if LeftPanState.lastState != .began && LeftPanState.lastState != .changed {
return
}
let translation: CGPoint = panGesture.translation(in: panGesture.view!)
leftContainerView.frame = applyLeftTranslation(translation, toFrame: LeftPanState.frameAtStartOfPan)
applyLeftOpacity()
applyLeftContentViewScale()
case UIGestureRecognizerState.ended, UIGestureRecognizerState.cancelled:
if LeftPanState.lastState != .changed {
setCloseWindowLevel()
return
}
let velocity:CGPoint = panGesture.velocity(in: panGesture.view)
let panInfo: PanInfo = panLeftResultInfoForVelocity(velocity)
if panInfo.action == .open {
if !LeftPanState.wasHiddenAtStartOfPan {
leftViewController?.beginAppearanceTransition(true, animated: true)
}
openLeftWithVelocity(panInfo.velocity)
track(.leftFlickOpen)
} else {
if LeftPanState.wasHiddenAtStartOfPan {
leftViewController?.beginAppearanceTransition(false, animated: true)
}
closeLeftWithVelocity(panInfo.velocity)
setCloseWindowLevel()
track(.leftFlickClose)
}
case UIGestureRecognizerState.failed, UIGestureRecognizerState.possible:
break
}
LeftPanState.lastState = panGesture.state
}
struct RightPanState {
static var frameAtStartOfPan: CGRect = CGRect.zero
static var startPointOfPan: CGPoint = CGPoint.zero
static var wasOpenAtStartOfPan: Bool = false
static var wasHiddenAtStartOfPan: Bool = false
static var lastState : UIGestureRecognizerState = .ended
}
@objc func handleRightPanGesture(_ panGesture: UIPanGestureRecognizer) {
if !isTagetViewController() {
return
}
if isLeftOpen() {
return
}
switch panGesture.state {
case UIGestureRecognizerState.began:
if RightPanState.lastState != .ended && RightPanState.lastState != .cancelled && RightPanState.lastState != .failed {
return
}
if isRightHidden() {
self.delegate?.rightWillOpen?()
} else {
self.delegate?.rightWillClose?()
}
RightPanState.frameAtStartOfPan = rightContainerView.frame
RightPanState.startPointOfPan = panGesture.location(in: view)
RightPanState.wasOpenAtStartOfPan = isRightOpen()
RightPanState.wasHiddenAtStartOfPan = isRightHidden()
rightViewController?.beginAppearanceTransition(RightPanState.wasHiddenAtStartOfPan, animated: true)
addShadowToView(rightContainerView)
setOpenWindowLevel()
case UIGestureRecognizerState.changed:
if RightPanState.lastState != .began && RightPanState.lastState != .changed {
return
}
let translation: CGPoint = panGesture.translation(in: panGesture.view!)
rightContainerView.frame = applyRightTranslation(translation, toFrame: RightPanState.frameAtStartOfPan)
applyRightOpacity()
applyRightContentViewScale()
case UIGestureRecognizerState.ended, UIGestureRecognizerState.cancelled:
if RightPanState.lastState != .changed {
setCloseWindowLevel()
return
}
let velocity: CGPoint = panGesture.velocity(in: panGesture.view)
let panInfo: PanInfo = panRightResultInfoForVelocity(velocity)
if panInfo.action == .open {
if !RightPanState.wasHiddenAtStartOfPan {
rightViewController?.beginAppearanceTransition(true, animated: true)
}
openRightWithVelocity(panInfo.velocity)
track(.rightFlickOpen)
} else {
if RightPanState.wasHiddenAtStartOfPan {
rightViewController?.beginAppearanceTransition(false, animated: true)
}
closeRightWithVelocity(panInfo.velocity)
setCloseWindowLevel()
track(.rightFlickClose)
}
case UIGestureRecognizerState.failed, UIGestureRecognizerState.possible:
break
}
RightPanState.lastState = panGesture.state
}
open func openLeftWithVelocity(_ velocity: CGFloat) {
let xOrigin: CGFloat = leftContainerView.frame.origin.x
let finalXOrigin: CGFloat = 0.0
var frame = leftContainerView.frame
frame.origin.x = finalXOrigin
var duration: TimeInterval = Double(SlideMenuOptions.animationDuration)
if velocity != 0.0 {
duration = Double(fabs(xOrigin - finalXOrigin) / velocity)
duration = Double(fmax(0.1, fmin(1.0, duration)))
}
addShadowToView(leftContainerView)
UIView.animate(withDuration: duration, delay: 0.0, options: SlideMenuOptions.animationOptions, animations: { [weak self]() -> Void in
if let strongSelf = self {
strongSelf.leftContainerView.frame = frame
strongSelf.opacityView.layer.opacity = Float(SlideMenuOptions.contentViewOpacity)
SlideMenuOptions.contentViewDrag == true ? (strongSelf.mainContainerView.transform = CGAffineTransform(translationX: SlideMenuOptions.leftViewWidth, y: 0)) : (strongSelf.mainContainerView.transform = CGAffineTransform(scaleX: SlideMenuOptions.contentViewScale, y: SlideMenuOptions.contentViewScale))
}
}) { [weak self](Bool) -> Void in
if let strongSelf = self {
strongSelf.disableContentInteraction()
strongSelf.leftViewController?.endAppearanceTransition()
strongSelf.delegate?.leftDidOpen?()
}
}
}
open func openRightWithVelocity(_ velocity: CGFloat) {
let xOrigin: CGFloat = rightContainerView.frame.origin.x
// CGFloat finalXOrigin = SlideMenuOptions.rightViewOverlapWidth
let finalXOrigin: CGFloat = view.bounds.width - rightContainerView.frame.size.width
var frame = rightContainerView.frame
frame.origin.x = finalXOrigin
var duration: TimeInterval = Double(SlideMenuOptions.animationDuration)
if velocity != 0.0 {
duration = Double(fabs(xOrigin - view.bounds.width) / velocity)
duration = Double(fmax(0.1, fmin(1.0, duration)))
}
addShadowToView(rightContainerView)
UIView.animate(withDuration: duration, delay: 0.0, options: SlideMenuOptions.animationOptions, animations: { [weak self]() -> Void in
if let strongSelf = self {
strongSelf.rightContainerView.frame = frame
strongSelf.opacityView.layer.opacity = Float(SlideMenuOptions.contentViewOpacity)
SlideMenuOptions.contentViewDrag == true ? (strongSelf.mainContainerView.transform = CGAffineTransform(translationX: -SlideMenuOptions.rightViewWidth, y: 0)) : (strongSelf.mainContainerView.transform = CGAffineTransform(scaleX: SlideMenuOptions.contentViewScale, y: SlideMenuOptions.contentViewScale))
}
}) { [weak self](Bool) -> Void in
if let strongSelf = self {
strongSelf.disableContentInteraction()
strongSelf.rightViewController?.endAppearanceTransition()
strongSelf.delegate?.rightDidOpen?()
}
}
}
open func closeLeftWithVelocity(_ velocity: CGFloat) {
let xOrigin: CGFloat = leftContainerView.frame.origin.x
let finalXOrigin: CGFloat = leftMinOrigin()
var frame: CGRect = leftContainerView.frame
frame.origin.x = finalXOrigin
var duration: TimeInterval = Double(SlideMenuOptions.animationDuration)
if velocity != 0.0 {
duration = Double(fabs(xOrigin - finalXOrigin) / velocity)
duration = Double(fmax(0.1, fmin(1.0, duration)))
}
UIView.animate(withDuration: duration, delay: 0.0, options: SlideMenuOptions.animationOptions, animations: { [weak self]() -> Void in
if let strongSelf = self {
strongSelf.leftContainerView.frame = frame
strongSelf.opacityView.layer.opacity = 0.0
strongSelf.mainContainerView.transform = CGAffineTransform(scaleX: 1.0, y: 1.0)
}
}) { [weak self](Bool) -> Void in
if let strongSelf = self {
strongSelf.removeShadow(strongSelf.leftContainerView)
strongSelf.enableContentInteraction()
strongSelf.leftViewController?.endAppearanceTransition()
strongSelf.delegate?.leftDidClose?()
}
}
}
open func closeRightWithVelocity(_ velocity: CGFloat) {
let xOrigin: CGFloat = rightContainerView.frame.origin.x
let finalXOrigin: CGFloat = view.bounds.width
var frame: CGRect = rightContainerView.frame
frame.origin.x = finalXOrigin
var duration: TimeInterval = Double(SlideMenuOptions.animationDuration)
if velocity != 0.0 {
duration = Double(fabs(xOrigin - view.bounds.width) / velocity)
duration = Double(fmax(0.1, fmin(1.0, duration)))
}
UIView.animate(withDuration: duration, delay: 0.0, options: SlideMenuOptions.animationOptions, animations: { [weak self]() -> Void in
if let strongSelf = self {
strongSelf.rightContainerView.frame = frame
strongSelf.opacityView.layer.opacity = 0.0
strongSelf.mainContainerView.transform = CGAffineTransform(scaleX: 1.0, y: 1.0)
}
}) { [weak self](Bool) -> Void in
if let strongSelf = self {
strongSelf.removeShadow(strongSelf.rightContainerView)
strongSelf.enableContentInteraction()
strongSelf.rightViewController?.endAppearanceTransition()
strongSelf.delegate?.rightDidClose?()
}
}
}
open override func toggleLeft() {
if isLeftOpen() {
closeLeft()
setCloseWindowLevel()
// Tracking of close tap is put in here. Because closeMenu is due to be call even when the menu tap.
track(.leftTapClose)
} else {
openLeft()
}
}
open func isLeftOpen() -> Bool {
return leftViewController != nil && leftContainerView.frame.origin.x == 0.0
}
open func isLeftHidden() -> Bool {
return leftContainerView.frame.origin.x <= leftMinOrigin()
}
open override func toggleRight() {
if isRightOpen() {
closeRight()
setCloseWindowLevel()
// Tracking of close tap is put in here. Because closeMenu is due to be call even when the menu tap.
track(.rightTapClose)
} else {
openRight()
}
}
open func isRightOpen() -> Bool {
return rightViewController != nil && rightContainerView.frame.origin.x == view.bounds.width - rightContainerView.frame.size.width
}
open func isRightHidden() -> Bool {
return rightContainerView.frame.origin.x >= view.bounds.width
}
open func changeMainViewController(_ mainViewController: UIViewController, close: Bool) {
removeViewController(self.mainViewController)
self.mainViewController = mainViewController
setUpViewController(mainContainerView, targetViewController: mainViewController)
if close {
closeLeft()
closeRight()
}
}
open func changeLeftViewWidth(_ width: CGFloat) {
SlideMenuOptions.leftViewWidth = width
var leftFrame: CGRect = view.bounds
leftFrame.size.width = width
leftFrame.origin.x = leftMinOrigin()
let leftOffset: CGFloat = 0
leftFrame.origin.y = leftFrame.origin.y + leftOffset
leftFrame.size.height = leftFrame.size.height - leftOffset
leftContainerView.frame = leftFrame
}
open func changeRightViewWidth(_ width: CGFloat) {
SlideMenuOptions.rightBezelWidth = width
var rightFrame: CGRect = view.bounds
rightFrame.size.width = width
rightFrame.origin.x = rightMinOrigin()
let rightOffset: CGFloat = 0
rightFrame.origin.y = rightFrame.origin.y + rightOffset
rightFrame.size.height = rightFrame.size.height - rightOffset
rightContainerView.frame = rightFrame
}
open func changeLeftViewController(_ leftViewController: UIViewController, closeLeft:Bool) {
removeViewController(self.leftViewController)
self.leftViewController = leftViewController
setUpViewController(leftContainerView, targetViewController: leftViewController)
if closeLeft {
self.closeLeft()
}
}
open func changeRightViewController(_ rightViewController: UIViewController, closeRight:Bool) {
removeViewController(self.rightViewController)
self.rightViewController = rightViewController
setUpViewController(rightContainerView, targetViewController: rightViewController)
if closeRight {
self.closeRight()
}
}
fileprivate func leftMinOrigin() -> CGFloat {
return -SlideMenuOptions.leftViewWidth
}
fileprivate func rightMinOrigin() -> CGFloat {
return view.bounds.width
}
fileprivate func panLeftResultInfoForVelocity(_ velocity: CGPoint) -> PanInfo {
let thresholdVelocity: CGFloat = 1000.0
let pointOfNoReturn: CGFloat = CGFloat(floor(leftMinOrigin())) + SlideMenuOptions.pointOfNoReturnWidth
let leftOrigin: CGFloat = leftContainerView.frame.origin.x
var panInfo: PanInfo = PanInfo(action: .close, shouldBounce: false, velocity: 0.0)
panInfo.action = leftOrigin <= pointOfNoReturn ? .close : .open
if velocity.x >= thresholdVelocity {
panInfo.action = .open
panInfo.velocity = velocity.x
} else if velocity.x <= (-1.0 * thresholdVelocity) {
panInfo.action = .close
panInfo.velocity = velocity.x
}
return panInfo
}
fileprivate func panRightResultInfoForVelocity(_ velocity: CGPoint) -> PanInfo {
let thresholdVelocity: CGFloat = -1000.0
let pointOfNoReturn: CGFloat = CGFloat(floor(view.bounds.width) - SlideMenuOptions.pointOfNoReturnWidth)
let rightOrigin: CGFloat = rightContainerView.frame.origin.x
var panInfo: PanInfo = PanInfo(action: .close, shouldBounce: false, velocity: 0.0)
panInfo.action = rightOrigin >= pointOfNoReturn ? .close : .open
if velocity.x <= thresholdVelocity {
panInfo.action = .open
panInfo.velocity = velocity.x
} else if velocity.x >= (-1.0 * thresholdVelocity) {
panInfo.action = .close
panInfo.velocity = velocity.x
}
return panInfo
}
fileprivate func applyLeftTranslation(_ translation: CGPoint, toFrame:CGRect) -> CGRect {
var newOrigin: CGFloat = toFrame.origin.x
newOrigin += translation.x
let minOrigin: CGFloat = leftMinOrigin()
let maxOrigin: CGFloat = 0.0
var newFrame: CGRect = toFrame
if newOrigin < minOrigin {
newOrigin = minOrigin
} else if newOrigin > maxOrigin {
newOrigin = maxOrigin
}
newFrame.origin.x = newOrigin
return newFrame
}
fileprivate func applyRightTranslation(_ translation: CGPoint, toFrame: CGRect) -> CGRect {
var newOrigin: CGFloat = toFrame.origin.x
newOrigin += translation.x
let minOrigin: CGFloat = rightMinOrigin()
let maxOrigin: CGFloat = rightMinOrigin() - rightContainerView.frame.size.width
var newFrame: CGRect = toFrame
if newOrigin > minOrigin {
newOrigin = minOrigin
} else if newOrigin < maxOrigin {
newOrigin = maxOrigin
}
newFrame.origin.x = newOrigin
return newFrame
}
fileprivate func getOpenedLeftRatio() -> CGFloat {
let width: CGFloat = leftContainerView.frame.size.width
let currentPosition: CGFloat = leftContainerView.frame.origin.x - leftMinOrigin()
return currentPosition / width
}
fileprivate func getOpenedRightRatio() -> CGFloat {
let width: CGFloat = rightContainerView.frame.size.width
let currentPosition: CGFloat = rightContainerView.frame.origin.x
return -(currentPosition - view.bounds.width) / width
}
fileprivate func applyLeftOpacity() {
let openedLeftRatio: CGFloat = getOpenedLeftRatio()
let opacity: CGFloat = SlideMenuOptions.contentViewOpacity * openedLeftRatio
opacityView.layer.opacity = Float(opacity)
}
fileprivate func applyRightOpacity() {
let openedRightRatio: CGFloat = getOpenedRightRatio()
let opacity: CGFloat = SlideMenuOptions.contentViewOpacity * openedRightRatio
opacityView.layer.opacity = Float(opacity)
}
fileprivate func applyLeftContentViewScale() {
let openedLeftRatio: CGFloat = getOpenedLeftRatio()
let scale: CGFloat = 1.0 - ((1.0 - SlideMenuOptions.contentViewScale) * openedLeftRatio)
let drag: CGFloat = SlideMenuOptions.leftViewWidth + leftContainerView.frame.origin.x
SlideMenuOptions.contentViewDrag == true ? (mainContainerView.transform = CGAffineTransform(translationX: drag, y: 0)) : (mainContainerView.transform = CGAffineTransform(scaleX: scale, y: scale))
}
fileprivate func applyRightContentViewScale() {
let openedRightRatio: CGFloat = getOpenedRightRatio()
let scale: CGFloat = 1.0 - ((1.0 - SlideMenuOptions.contentViewScale) * openedRightRatio)
let drag: CGFloat = rightContainerView.frame.origin.x - mainContainerView.frame.size.width
SlideMenuOptions.contentViewDrag == true ? (mainContainerView.transform = CGAffineTransform(translationX: drag, y: 0)) : (mainContainerView.transform = CGAffineTransform(scaleX: scale, y: scale))
}
fileprivate func addShadowToView(_ targetContainerView: UIView) {
targetContainerView.layer.masksToBounds = false
targetContainerView.layer.shadowOffset = SlideMenuOptions.shadowOffset
targetContainerView.layer.shadowOpacity = Float(SlideMenuOptions.shadowOpacity)
targetContainerView.layer.shadowRadius = SlideMenuOptions.shadowRadius
targetContainerView.layer.shadowPath = UIBezierPath(rect: targetContainerView.bounds).cgPath
}
fileprivate func removeShadow(_ targetContainerView: UIView) {
targetContainerView.layer.masksToBounds = true
mainContainerView.layer.opacity = 1.0
}
fileprivate func removeContentOpacity() {
opacityView.layer.opacity = 0.0
}
fileprivate func addContentOpacity() {
opacityView.layer.opacity = Float(SlideMenuOptions.contentViewOpacity)
}
fileprivate func disableContentInteraction() {
mainContainerView.isUserInteractionEnabled = false
}
fileprivate func enableContentInteraction() {
mainContainerView.isUserInteractionEnabled = true
}
fileprivate func setOpenWindowLevel() {
if SlideMenuOptions.hideStatusBar {
DispatchQueue.main.async(execute: {
if let window = UIApplication.shared.keyWindow {
window.windowLevel = UIWindowLevelStatusBar + 1
}
})
}
}
fileprivate func setCloseWindowLevel() {
if SlideMenuOptions.hideStatusBar {
DispatchQueue.main.async(execute: {
if let window = UIApplication.shared.keyWindow {
window.windowLevel = UIWindowLevelNormal
}
})
}
}
fileprivate func setUpViewController(_ targetView: UIView, targetViewController: UIViewController?) {
if let viewController = targetViewController {
viewController.view.frame = targetView.bounds
if (!childViewControllers.contains(viewController)) {
addChildViewController(viewController)
targetView.addSubview(viewController.view)
viewController.didMove(toParentViewController: self)
}
}
}
fileprivate func removeViewController(_ viewController: UIViewController?) {
if let _viewController = viewController {
_viewController.view.layer.removeAllAnimations()
_viewController.willMove(toParentViewController: nil)
_viewController.view.removeFromSuperview()
_viewController.removeFromParentViewController()
}
}
open func closeLeftNonAnimation(){
setCloseWindowLevel()
let finalXOrigin: CGFloat = leftMinOrigin()
var frame: CGRect = leftContainerView.frame
frame.origin.x = finalXOrigin
leftContainerView.frame = frame
opacityView.layer.opacity = 0.0
mainContainerView.transform = CGAffineTransform(scaleX: 1.0, y: 1.0)
removeShadow(leftContainerView)
enableContentInteraction()
}
open func closeRightNonAnimation(){
setCloseWindowLevel()
let finalXOrigin: CGFloat = view.bounds.width
var frame: CGRect = rightContainerView.frame
frame.origin.x = finalXOrigin
rightContainerView.frame = frame
opacityView.layer.opacity = 0.0
mainContainerView.transform = CGAffineTransform(scaleX: 1.0, y: 1.0)
removeShadow(rightContainerView)
enableContentInteraction()
}
// MARK: UIGestureRecognizerDelegate
open func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool {
let point: CGPoint = touch.location(in: view)
if gestureRecognizer == leftPanGesture {
return slideLeftForGestureRecognizer(gestureRecognizer, point: point)
} else if gestureRecognizer == rightPanGesture {
return slideRightViewForGestureRecognizer(gestureRecognizer, withTouchPoint: point)
} else if gestureRecognizer == leftTapGesture {
return isLeftOpen() && !isPointContainedWithinLeftRect(point)
} else if gestureRecognizer == rightTapGesture {
return isRightOpen() && !isPointContainedWithinRightRect(point)
}
return true
}
// returning true here helps if the main view is fullwidth with a scrollview
open func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return SlideMenuOptions.simultaneousGestureRecognizers
}
fileprivate func slideLeftForGestureRecognizer( _ gesture: UIGestureRecognizer, point:CGPoint) -> Bool{
return isLeftOpen() || SlideMenuOptions.panFromBezel && isLeftPointContainedWithinBezelRect(point)
}
fileprivate func isLeftPointContainedWithinBezelRect(_ point: CGPoint) -> Bool{
if let bezelWidth = SlideMenuOptions.leftBezelWidth {
var leftBezelRect: CGRect = CGRect.zero
let tuple = view.bounds.divided(atDistance: bezelWidth, from: CGRectEdge.minXEdge)
leftBezelRect = tuple.slice
return leftBezelRect.contains(point)
} else {
return true
}
}
fileprivate func isPointContainedWithinLeftRect(_ point: CGPoint) -> Bool {
return leftContainerView.frame.contains(point)
}
fileprivate func slideRightViewForGestureRecognizer(_ gesture: UIGestureRecognizer, withTouchPoint point: CGPoint) -> Bool {
return isRightOpen() || SlideMenuOptions.rightPanFromBezel && isRightPointContainedWithinBezelRect(point)
}
fileprivate func isRightPointContainedWithinBezelRect(_ point: CGPoint) -> Bool {
if let rightBezelWidth = SlideMenuOptions.rightBezelWidth {
var rightBezelRect: CGRect = CGRect.zero
let bezelWidth: CGFloat = view.bounds.width - rightBezelWidth
let tuple = view.bounds.divided(atDistance: bezelWidth, from: CGRectEdge.minXEdge)
rightBezelRect = tuple.remainder
return rightBezelRect.contains(point)
} else {
return true
}
}
fileprivate func isPointContainedWithinRightRect(_ point: CGPoint) -> Bool {
return rightContainerView.frame.contains(point)
}
}
extension UIViewController {
public func slideMenuController() -> SlideMenuController? {
var viewController: UIViewController? = self
while viewController != nil {
if viewController is SlideMenuController {
return viewController as? SlideMenuController
}
viewController = viewController?.parent
}
return nil
}
public func addLeftBarButtonWithImage(_ buttonImage: UIImage) {
let leftButton: UIBarButtonItem = UIBarButtonItem(image: buttonImage, style: UIBarButtonItemStyle.plain, target: self, action: #selector(self.toggleLeft))
navigationItem.leftBarButtonItem = leftButton
}
public func addRightBarButtonWithImage(_ buttonImage: UIImage) {
let rightButton: UIBarButtonItem = UIBarButtonItem(image: buttonImage, style: UIBarButtonItemStyle.plain, target: self, action: #selector(self.toggleRight))
navigationItem.rightBarButtonItem = rightButton
}
@objc public func toggleLeft() {
slideMenuController()?.toggleLeft()
}
@objc public func toggleRight() {
slideMenuController()?.toggleRight()
}
@objc public func openLeft() {
slideMenuController()?.openLeft()
}
@objc public func openRight() {
slideMenuController()?.openRight() }
@objc public func closeLeft() {
slideMenuController()?.closeLeft()
}
@objc public func closeRight() {
slideMenuController()?.closeRight()
}
// Please specify if you want menu gesuture give priority to than targetScrollView
public func addPriorityToMenuGesuture(_ targetScrollView: UIScrollView) {
guard let slideController = slideMenuController(), let recognizers = slideController.view.gestureRecognizers else {
return
}
for recognizer in recognizers where recognizer is UIPanGestureRecognizer {
targetScrollView.panGestureRecognizer.require(toFail: recognizer)
}
}
}