added iOS source code
[wl-app.git] / iOS / Pods / SideMenu / Pod / Classes / SideMenuTransition.swift
1 //
2 //  SideMenuTransition.swift
3 //  Pods
4 //
5 //  Created by Jon Kent on 1/14/16.
6 //
7 //
8
9 import UIKit
10
11 open class SideMenuTransition: UIPercentDrivenInteractiveTransition {
12     
13     fileprivate var presenting = false
14     fileprivate var interactive = false
15     fileprivate weak var originalSuperview: UIView?
16     fileprivate weak var activeGesture: UIGestureRecognizer?
17     fileprivate var switchMenus = false {
18         didSet {
19             if switchMenus {
20                 cancel()
21             }
22         }
23     }
24     fileprivate var menuWidth: CGFloat {
25         get {
26             let overriddenWidth = menuViewController?.menuWidth ?? 0
27             if overriddenWidth > CGFloat.ulpOfOne {
28                 return overriddenWidth
29             }
30             return sideMenuManager.menuWidth
31         }
32     }
33     internal weak var sideMenuManager: SideMenuManager!
34     internal weak var mainViewController: UIViewController?
35     internal weak var menuViewController: UISideMenuNavigationController? {
36         get {
37             return presentDirection == .left ? sideMenuManager.menuLeftNavigationController : sideMenuManager.menuRightNavigationController
38         }
39     }
40     internal var presentDirection: UIRectEdge = .left
41     internal weak var tapView: UIView? {
42         didSet {
43             guard let tapView = tapView else {
44                 return
45             }
46             
47             tapView.autoresizingMask = [.flexibleHeight, .flexibleWidth]
48             let exitPanGesture = UIPanGestureRecognizer()
49             exitPanGesture.addTarget(self, action:#selector(SideMenuTransition.handleHideMenuPan(_:)))
50             let exitTapGesture = UITapGestureRecognizer()
51             exitTapGesture.addTarget(self, action: #selector(SideMenuTransition.handleHideMenuTap(_:)))
52             tapView.addGestureRecognizer(exitPanGesture)
53             tapView.addGestureRecognizer(exitTapGesture)
54         }
55     }
56     internal weak var statusBarView: UIView? {
57         didSet {
58             guard let statusBarView = statusBarView else {
59                 return
60             }
61             
62             statusBarView.backgroundColor = sideMenuManager.menuAnimationBackgroundColor ?? UIColor.black
63             statusBarView.isUserInteractionEnabled = false
64         }
65     }
66     
67     required public init(sideMenuManager: SideMenuManager) {
68         super.init()
69         
70         NotificationCenter.default.addObserver(self, selector:#selector(handleNotification), name: NSNotification.Name.UIApplicationDidEnterBackground, object: nil)
71         NotificationCenter.default.addObserver(self, selector:#selector(handleNotification), name: NSNotification.Name.UIApplicationWillChangeStatusBarFrame, object: nil)
72         self.sideMenuManager = sideMenuManager
73     }
74     
75     deinit {
76         NotificationCenter.default.removeObserver(self)
77     }
78     
79     fileprivate static var visibleViewController: UIViewController? {
80         get {
81             return getVisibleViewController(forViewController: UIApplication.shared.keyWindow?.rootViewController)
82         }
83     }
84     
85     fileprivate class func getVisibleViewController(forViewController: UIViewController?) -> UIViewController? {
86         if let navigationController = forViewController as? UINavigationController {
87             return getVisibleViewController(forViewController: navigationController.visibleViewController)
88         }
89         if let tabBarController = forViewController as? UITabBarController {
90             return getVisibleViewController(forViewController: tabBarController.selectedViewController)
91         }
92         if let splitViewController = forViewController as? UISplitViewController {
93             return getVisibleViewController(forViewController: splitViewController.viewControllers.last)
94         }
95         if let presentedViewController = forViewController?.presentedViewController {
96             return getVisibleViewController(forViewController: presentedViewController)
97         }
98         
99         return forViewController
100     }
101     
102     @objc internal func handlePresentMenuLeftScreenEdge(_ edge: UIScreenEdgePanGestureRecognizer) {
103         presentDirection = .left
104         handlePresentMenuPan(edge)
105     }
106     
107     @objc internal func handlePresentMenuRightScreenEdge(_ edge: UIScreenEdgePanGestureRecognizer) {
108         presentDirection = .right
109         handlePresentMenuPan(edge)
110     }
111     
112     @objc internal func handlePresentMenuPan(_ pan: UIPanGestureRecognizer) {
113         if activeGesture == nil {
114             activeGesture = pan
115         } else if pan != activeGesture {
116             pan.isEnabled = false
117             pan.isEnabled = true
118             return
119         } else if pan.state != .began && pan.state != .changed {
120             activeGesture = nil
121         }
122         
123         // how much distance have we panned in reference to the parent view?
124         guard let view = mainViewController?.view ?? pan.view else {
125             return
126         }
127         
128         let transform = view.transform
129         view.transform = .identity
130         let translation = pan.translation(in: pan.view!)
131         view.transform = transform
132         
133         // do some math to translate this to a percentage based value
134         if !interactive {
135             if translation.x == 0 {
136                 return // not sure which way the user is swiping yet, so do nothing
137             }
138             
139             if !(pan is UIScreenEdgePanGestureRecognizer) {
140                 presentDirection = translation.x > 0 ? .left : .right
141             }
142             
143             if let menuViewController = menuViewController, let visibleViewController = SideMenuTransition.visibleViewController {
144                 interactive = true
145                 visibleViewController.present(menuViewController, animated: true, completion: nil)
146             } else {
147                 return
148             }
149         }
150         
151         let direction: CGFloat = presentDirection == .left ? 1 : -1
152         let distance = translation.x / menuWidth
153         // now lets deal with different states that the gesture recognizer sends
154         switch (pan.state) {
155         case .began, .changed:
156             if pan is UIScreenEdgePanGestureRecognizer {
157                 update(min(distance * direction, 1))
158             } else if distance > 0 && presentDirection == .right && sideMenuManager.menuLeftNavigationController != nil {
159                 presentDirection = .left
160                 switchMenus = true
161             } else if distance < 0 && presentDirection == .left && sideMenuManager.menuRightNavigationController != nil {
162                 presentDirection = .right
163                 switchMenus = true
164             } else {
165                 update(min(distance * direction, 1))
166             }
167         default:
168             interactive = false
169             view.transform = .identity
170             let velocity = pan.velocity(in: pan.view!).x * direction
171             view.transform = transform
172             if velocity >= 100 || velocity >= -50 && abs(distance) >= 0.5 {
173                 // bug workaround: animation briefly resets after call to finishInteractiveTransition() but before animateTransition completion is called.
174                 if ProcessInfo().operatingSystemVersion.majorVersion == 8 && percentComplete > 1 - CGFloat.ulpOfOne {
175                     update(0.9999)
176                 }
177                 finish()
178             } else {
179                 cancel()
180             }
181         }
182     }
183     
184     @objc internal func handleHideMenuPan(_ pan: UIPanGestureRecognizer) {
185         if activeGesture == nil {
186             activeGesture = pan
187         } else if pan != activeGesture {
188             pan.isEnabled = false
189             pan.isEnabled = true
190             return
191         }
192         
193         let translation = pan.translation(in: pan.view!)
194         let direction:CGFloat = presentDirection == .left ? -1 : 1
195         let distance = translation.x / menuWidth * direction
196         
197         switch (pan.state) {
198             
199         case .began:
200             interactive = true
201             mainViewController?.dismiss(animated: true, completion: nil)
202         case .changed:
203             update(max(min(distance, 1), 0))
204         default:
205             interactive = false
206             let velocity = pan.velocity(in: pan.view!).x * direction
207             if velocity >= 100 || velocity >= -50 && distance >= 0.5 {
208                 // bug workaround: animation briefly resets after call to finishInteractiveTransition() but before animateTransition completion is called.
209                 if ProcessInfo().operatingSystemVersion.majorVersion == 8 && percentComplete > 1 - CGFloat.ulpOfOne {
210                     update(0.9999)
211                 }
212                 finish()
213                 activeGesture = nil
214             } else {
215                 cancel()
216                 activeGesture = nil
217             }
218         }
219     }
220     
221     @objc internal func handleHideMenuTap(_ tap: UITapGestureRecognizer) {
222         menuViewController?.dismiss(animated: true, completion: nil)
223     }
224     
225     @discardableResult internal func hideMenuStart() -> SideMenuTransition {
226         let menuView = menuViewController?.view
227         let mainView = mainViewController?.view
228       
229         mainView?.transform = .identity
230         mainView?.alpha = 1
231         mainView?.frame.origin = .zero
232         menuView?.transform = .identity
233         menuView?.frame.origin.y = 0
234         menuView?.frame.size.width = menuWidth
235         menuView?.frame.size.height = mainView?.frame.height ?? 0 // in case status bar height changed
236         var statusBarFrame = UIApplication.shared.statusBarFrame
237         let statusBarOffset = SideMenuManager.appScreenRect.size.height - (mainView?.frame.maxY ?? 0)
238         // For in-call status bar, height is normally 40, which overlaps view. Instead, calculate height difference
239         // of view and set height to fill in remaining space.
240         if statusBarOffset >= CGFloat.ulpOfOne {
241             statusBarFrame.size.height = statusBarOffset
242         }
243         statusBarView?.frame = statusBarFrame
244         statusBarView?.alpha = 0
245         
246         switch sideMenuManager.menuPresentMode {
247             
248         case .viewSlideOut:
249             menuView?.alpha = 1 - sideMenuManager.menuAnimationFadeStrength
250             menuView?.frame.origin.x = presentDirection == .left ? 0 : (mainView?.frame.width ?? 0) - menuWidth
251             menuView?.transform = CGAffineTransform(scaleX: sideMenuManager.menuAnimationTransformScaleFactor, y: sideMenuManager.menuAnimationTransformScaleFactor)
252             
253         case .viewSlideInOut:
254             menuView?.alpha = 1
255             menuView?.frame.origin.x = presentDirection == .left ? -menuView!.frame.width : mainView!.frame.width
256             
257         case .menuSlideIn:
258             menuView?.alpha = 1
259             menuView?.frame.origin.x = presentDirection == .left ? -menuView!.frame.width : mainView!.frame.width
260             
261         case .menuDissolveIn:
262             menuView?.alpha = 0
263             menuView?.frame.origin.x = presentDirection == .left ? 0 : mainView!.frame.width - menuWidth
264         }
265         
266         return self
267     }
268     
269     @discardableResult internal func hideMenuComplete() -> SideMenuTransition {
270         let menuView = menuViewController?.view
271         let mainView = mainViewController?.view
272
273         tapView?.removeFromSuperview()
274         statusBarView?.removeFromSuperview()
275         mainView?.motionEffects.removeAll()
276         mainView?.layer.shadowOpacity = 0
277         menuView?.layer.shadowOpacity = 0
278         if let topNavigationController = mainViewController as? UINavigationController {
279             topNavigationController.interactivePopGestureRecognizer!.isEnabled = true
280         }
281         if let originalSuperview = originalSuperview, let mainView = mainViewController?.view {
282             originalSuperview.addSubview(mainView)
283             let y = originalSuperview.bounds.height - mainView.frame.size.height
284             mainView.frame.origin.y = max(y, 0)
285         }
286         
287         originalSuperview = nil
288         mainViewController = nil
289         
290         return self
291     }
292     
293     @discardableResult internal func presentMenuStart() -> SideMenuTransition {
294         let menuView = menuViewController?.view
295         let mainView = mainViewController?.view
296         
297         menuView?.alpha = 1
298         menuView?.transform = .identity
299         menuView?.frame.size.width = menuWidth
300         let size = SideMenuManager.appScreenRect.size
301         menuView?.frame.origin.x = presentDirection == .left ? 0 : size.width - menuWidth
302         mainView?.transform = .identity
303         mainView?.frame.size.width = size.width
304         let statusBarOffset = size.height - (menuView?.bounds.height ?? 0)
305         mainView?.bounds.size.height = size.height - max(statusBarOffset, 0)
306         mainView?.frame.origin.y = 0
307         var statusBarFrame = UIApplication.shared.statusBarFrame
308         // For in-call status bar, height is normally 40, which overlaps view. Instead, calculate height difference
309         // of view and set height to fill in remaining space.
310         if statusBarOffset >= CGFloat.ulpOfOne {
311             statusBarFrame.size.height = statusBarOffset
312         }
313         tapView?.transform = .identity
314         tapView?.bounds = mainView!.bounds
315         statusBarView?.frame = statusBarFrame
316         statusBarView?.alpha = 1
317         
318         switch sideMenuManager.menuPresentMode {
319             
320         case .viewSlideOut, .viewSlideInOut:
321             mainView?.layer.shadowColor = sideMenuManager.menuShadowColor.cgColor
322             mainView?.layer.shadowRadius = sideMenuManager.menuShadowRadius
323             mainView?.layer.shadowOpacity = sideMenuManager.menuShadowOpacity
324             mainView?.layer.shadowOffset = CGSize(width: 0, height: 0)
325             let direction:CGFloat = presentDirection == .left ? 1 : -1
326             mainView?.frame.origin.x = direction * (menuView!.frame.width)
327             
328         case .menuSlideIn, .menuDissolveIn:
329             if sideMenuManager.menuBlurEffectStyle == nil {
330                 menuView?.layer.shadowColor = sideMenuManager.menuShadowColor.cgColor
331                 menuView?.layer.shadowRadius = sideMenuManager.menuShadowRadius
332                 menuView?.layer.shadowOpacity = sideMenuManager.menuShadowOpacity
333                 menuView?.layer.shadowOffset = CGSize(width: 0, height: 0)
334             }
335             mainView?.frame.origin.x = 0
336         }
337         
338         if sideMenuManager.menuPresentMode != .viewSlideOut {
339             mainView?.transform = CGAffineTransform(scaleX: sideMenuManager.menuAnimationTransformScaleFactor, y: sideMenuManager.menuAnimationTransformScaleFactor)
340             if sideMenuManager.menuAnimationTransformScaleFactor > 1 {
341                 tapView?.transform = mainView!.transform
342             }
343             mainView?.alpha = 1 - sideMenuManager.menuAnimationFadeStrength
344         }
345         
346         return self
347     }
348     
349     @discardableResult internal func presentMenuComplete() -> SideMenuTransition {
350         switch sideMenuManager.menuPresentMode {
351         case .menuSlideIn, .menuDissolveIn, .viewSlideInOut:
352             if let mainView = mainViewController?.view, sideMenuManager.menuParallaxStrength != 0 {
353                 let horizontal = UIInterpolatingMotionEffect(keyPath: "center.x", type: .tiltAlongHorizontalAxis)
354                 horizontal.minimumRelativeValue = -sideMenuManager.menuParallaxStrength
355                 horizontal.maximumRelativeValue = sideMenuManager.menuParallaxStrength
356                 
357                 let vertical = UIInterpolatingMotionEffect(keyPath: "center.y", type: .tiltAlongVerticalAxis)
358                 vertical.minimumRelativeValue = -sideMenuManager.menuParallaxStrength
359                 vertical.maximumRelativeValue = sideMenuManager.menuParallaxStrength
360                 
361                 let group = UIMotionEffectGroup()
362                 group.motionEffects = [horizontal, vertical]
363                 mainView.addMotionEffect(group)
364             }
365         case .viewSlideOut: break;
366         }
367         if let topNavigationController = mainViewController as? UINavigationController {
368             topNavigationController.interactivePopGestureRecognizer!.isEnabled = false
369         }
370         
371         return self
372     }
373     
374     @objc internal func handleNotification(notification: NSNotification) {
375         guard menuViewController?.presentedViewController == nil &&
376             menuViewController?.presentingViewController != nil else {
377                 return
378         }
379         
380         if let originalSuperview = originalSuperview, let mainViewController = mainViewController {
381             originalSuperview.addSubview(mainViewController.view)
382         }
383         
384         if notification.name == NSNotification.Name.UIApplicationDidEnterBackground {
385             hideMenuStart().hideMenuComplete()
386             menuViewController?.dismiss(animated: false, completion: nil)
387             return
388         }
389         
390         UIView.animate(withDuration: sideMenuManager.menuAnimationDismissDuration,
391                        delay: 0,
392                        usingSpringWithDamping: sideMenuManager.menuAnimationUsingSpringWithDamping,
393                        initialSpringVelocity: sideMenuManager.menuAnimationInitialSpringVelocity,
394                        options: sideMenuManager.menuAnimationOptions,
395                        animations: {
396                         self.hideMenuStart()
397         }) { (finished) -> Void in
398             self.hideMenuComplete()
399             self.menuViewController?.dismiss(animated: false, completion: nil)
400         }
401     }
402     
403 }
404
405 extension SideMenuTransition: UIViewControllerAnimatedTransitioning {
406     
407     // animate a change from one viewcontroller to another
408     open func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
409         
410         // get reference to our fromView, toView and the container view that we should perform the transition in
411         let container = transitionContext.containerView
412         // prevent any other menu gestures from firing
413         container.isUserInteractionEnabled = false
414         
415         if let menuBackgroundColor = sideMenuManager.menuAnimationBackgroundColor {
416             container.backgroundColor = menuBackgroundColor
417         }
418         
419         let fromViewController = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.from)!
420         let toViewController = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.to)!
421         
422         // assign references to our menu view controller and the 'bottom' view controller from the tuple
423         // remember that our menuViewController will alternate between the from and to view controller depending if we're presenting or dismissing
424         mainViewController = presenting ? fromViewController : toViewController
425         
426         let menuView = menuViewController!.view!
427         let topView = mainViewController!.view!
428         
429         // prepare menu items to slide in
430         if presenting {
431             originalSuperview = topView.superview
432             
433             // add the both views to our view controller
434             switch sideMenuManager.menuPresentMode {
435             case .viewSlideOut, .viewSlideInOut:
436                 container.addSubview(menuView)
437                 container.addSubview(topView)
438             case .menuSlideIn, .menuDissolveIn:
439                 container.addSubview(topView)
440                 container.addSubview(menuView)
441             }
442
443             if sideMenuManager.menuFadeStatusBar {
444                 let statusBarView = UIView()
445                 self.statusBarView = statusBarView
446                 container.addSubview(statusBarView)
447             }
448             
449             hideMenuStart()
450         }
451         
452         let animate = {
453             if self.presenting {
454                 self.presentMenuStart()
455             } else {
456                 self.hideMenuStart()
457             }
458         }
459         
460         let complete = {
461             container.isUserInteractionEnabled = true
462             
463             // tell our transitionContext object that we've finished animating
464             if transitionContext.transitionWasCancelled {
465                 let viewControllerForPresentedMenu = self.mainViewController
466                 
467                 if self.presenting {
468                     self.hideMenuComplete()
469                 } else {
470                     self.presentMenuComplete()
471                 }
472                 
473                 transitionContext.completeTransition(false)
474                 
475                 if self.switchMenus {
476                     self.switchMenus = false
477                     viewControllerForPresentedMenu?.present(self.menuViewController!, animated: true, completion: nil)
478                 }
479                 
480                 return
481             }
482             
483             if self.presenting {
484                 self.presentMenuComplete()
485                 transitionContext.completeTransition(true)
486                 switch self.sideMenuManager.menuPresentMode {
487                 case .viewSlideOut, .viewSlideInOut:
488                     container.addSubview(topView)
489                 case .menuSlideIn, .menuDissolveIn:
490                     container.insertSubview(topView, at: 0)
491                 }
492                 if !self.sideMenuManager.menuPresentingViewControllerUserInteractionEnabled {
493                     let tapView = UIView()
494                     container.insertSubview(tapView, aboveSubview: topView)
495                     tapView.bounds = container.bounds
496                     tapView.center = topView.center
497                     if self.sideMenuManager.menuAnimationTransformScaleFactor > 1 {
498                         tapView.transform = topView.transform
499                     }
500                     self.tapView = tapView
501                 }
502                 if let statusBarView = self.statusBarView {
503                     container.bringSubview(toFront: statusBarView)
504                 }
505                 
506                 return
507             }
508             
509             self.hideMenuComplete()
510             transitionContext.completeTransition(true)
511             menuView.removeFromSuperview()
512         }
513         
514         // perform the animation!
515         let duration = transitionDuration(using: transitionContext)
516         if interactive {
517             UIView.animate(withDuration: duration,
518                            delay: duration, // HACK: If zero, the animation briefly flashes in iOS 11. UIViewPropertyAnimators (iOS 10+) may resolve this.
519                            options: .curveLinear,
520                            animations: {
521                             animate()
522             }, completion: { (finished) in
523                 complete()
524             })
525         } else {
526             UIView.animate(withDuration: duration,
527                            delay: 0,
528                            usingSpringWithDamping: sideMenuManager.menuAnimationUsingSpringWithDamping,
529                            initialSpringVelocity: sideMenuManager.menuAnimationInitialSpringVelocity,
530                            options: sideMenuManager.menuAnimationOptions,
531                            animations: {
532                             animate()
533             }) { (finished) -> Void in
534                 complete()
535             }
536         }
537     }
538     
539     // return how many seconds the transiton animation will take
540     open func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
541         if interactive {
542             return sideMenuManager.menuAnimationCompleteGestureDuration
543         }
544         return presenting ? sideMenuManager.menuAnimationPresentDuration : sideMenuManager.menuAnimationDismissDuration
545     }
546     
547     open override func update(_ percentComplete: CGFloat) {
548         guard !switchMenus else {
549             return
550         }
551         
552         super.update(percentComplete)
553     }
554     
555 }
556
557 extension SideMenuTransition: UIViewControllerTransitioningDelegate {
558     
559     // return the animator when presenting a viewcontroller
560     // rememeber that an animator (or animation controller) is any object that aheres to the UIViewControllerAnimatedTransitioning protocol
561     open func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
562         self.presenting = true
563         presentDirection = presented == sideMenuManager.menuLeftNavigationController ? .left : .right
564         return self
565     }
566     
567     // return the animator used when dismissing from a viewcontroller
568     open func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
569         presenting = false
570         return self
571     }
572     
573     open func interactionControllerForPresentation(using animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {
574         // if our interactive flag is true, return the transition manager object
575         // otherwise return nil
576         return interactive ? self : nil
577     }
578     
579     open func interactionControllerForDismissal(using animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {
580         return interactive ? self : nil
581     }
582     
583 }