kocoai
3/19/2017 - 5:36 PM

[Transition] #tags: swift, transition

[Transition] #tags: swift, transition

//
//  ModalInteractiveTransition.swift
//  iOSSportModule
//
//  Created by Kien Nguyen on 25/01/2017.
//  Copyright © 2017 Kien Nguyen. All rights reserved.
//

import UIKit

class ModalInteractiveTransition: UIPercentDrivenInteractiveTransition, UIGestureRecognizerDelegate {

    enum ModalInteractiveTransitionConfiguration {
        case presentFromBottom
        case presentFromTop
        
        static let transitionDuration: TimeInterval = 1.0
        static let dampingRatio: CGFloat = 0.7
        static let velocity: CGFloat = 0.2
        
    }
    
    private var panLocationStart: CGFloat = 0
    fileprivate var transitionContext: UIViewControllerContextTransitioning?
    fileprivate var isInteractionInProgress = false
    fileprivate var isDismiss = false
    private var panGesture: UIPanGestureRecognizer?
    fileprivate var configuration = ModalInteractiveTransitionConfiguration.presentFromTop
    fileprivate weak var modalController: UIViewController!
    
    // setup the vc with animator
    func setupWithViewController(modalController: UIViewController!, configuration: ModalInteractiveTransitionConfiguration = .presentFromTop) {
        removeGestureRecognizerFromModalController()
        self.configuration = configuration
        self.modalController = modalController
        self.modalController.transitioningDelegate = self
        self.panGesture = UIPanGestureRecognizer(target: self, action: #selector(handleGesture(gestureRecognizer:)))
        guard let gesture = self.panGesture else {
            return
        }
        gesture.delegate = self
        self.modalController.view.addGestureRecognizer(gesture)
    }
    
    // animation
    fileprivate func animate(animations: @escaping () -> Swift.Void, completion: ((Bool) -> Swift.Void)? = nil) {
        UIView.animate(withDuration: ModalInteractiveTransitionConfiguration.transitionDuration,
                       delay: 0,
                       usingSpringWithDamping: ModalInteractiveTransitionConfiguration.dampingRatio,
                       initialSpringVelocity: ModalInteractiveTransitionConfiguration.velocity,
                       options: UIViewAnimationOptions.curveEaseInOut,
                       animations: animations,
                       completion:completion)
    }
    
    // setup gesture
    func handleGesture(gestureRecognizer: UIPanGestureRecognizer) {
        guard let view  = self.modalController.view.window else {
            return
        }
        
        let location = gestureRecognizer.location(in: view)
        let velocity = gestureRecognizer.velocity(in: view)
        let viewHeight = view.bounds.height
        
        switch gestureRecognizer.state {
        case .began:
            isInteractionInProgress = true
            panLocationStart = location.y
            modalController.dismiss(animated: true, completion: nil)
        case .changed:
            let animationRatio = (location.y - self.panLocationStart) / viewHeight
            update(animationRatio)
        case .ended:
            isInteractionInProgress = false
            let velocityForSelectedDirection = velocity.y
            if abs(velocityForSelectedDirection) > 100 {
                finish(shouldGoDown: velocityForSelectedDirection > 0)
            } else {
                cancel()
            }
        default:
            print("Unsupported")
        }
    }
    
    private func removeGestureRecognizerFromModalController() {
        guard let gesture = self.panGesture,
            let vc = modalController,
            let gestureRecognizers = vc.view.gestureRecognizers else {
                return
        }
        
        if gestureRecognizers.contains(gesture) {
            modalController.view.removeGestureRecognizer(gesture)
            self.panGesture = nil
        }
    }
    
    override func cancel() {
        guard let transitionContext = transitionContext,
            let fromViewController = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.from),
            let toViewController = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.to),
            let fromView = fromViewController.view
            else {
                return
        }
        
        transitionContext.cancelInteractiveTransition()
        animate(animations: {
            fromView.frame = CGRect(x: 0, y: 0,
                                    width: fromView.frame.width,
                                    height: fromView.frame.height)
        }, completion: { _ in
            transitionContext.completeTransition(false)
            if fromViewController.modalPresentationStyle == .fullScreen {
                toViewController.view.removeFromSuperview()
            }
        })
    }
    
    func finish(shouldGoDown goDown: Bool) {
        guard let transitionContext = transitionContext,
            let fromViewController = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.from),
            let toViewController = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.to),
            let fromView = fromViewController.view
            else {
                return
        }
        
        let y = goDown ? fromView.bounds.height : -fromView.bounds.height
        
        var endRect = CGRect(x: 0,
                             y: y,
                             width: fromView.frame.width,
                             height: fromView.frame.height)
        
        let transformedPoint = endRect.origin.applying(fromView.transform)
        endRect = CGRect(x: transformedPoint.x,
                         y: transformedPoint.y,
                         width: endRect.size.width,
                         height: endRect.size.height)
        
        if fromViewController.modalPresentationStyle == .custom {
            toViewController.beginAppearanceTransition(true, animated: true)
        }
        
        animate(animations: {
            fromView.frame = endRect
        }, completion: { _ in
            if fromViewController.modalPresentationStyle == .custom {
                toViewController.endAppearanceTransition()
            }
            transitionContext.completeTransition(true)
        })
    }
    
    override func update(_ percentComplete: CGFloat) {
        guard let fromViewController = transitionContext?.viewController(forKey: UITransitionContextViewControllerKey.from),
            let fromView = fromViewController.view
            else {
                return
        }
        var updateRect = CGRect(x: 0,
                                y: fromView.bounds.height * percentComplete,
                                width: fromView.frame.width,
                                height: fromView.frame.height)
        let transformedPoint = updateRect.origin.applying(fromViewController.view.transform)
        
        updateRect = CGRect(x: transformedPoint.x,
                            y: transformedPoint.y,
                            width: updateRect.width,
                            height: updateRect.height)
        fromView.frame = updateRect
    }
    
    //UIViewControllerInteractiveTransitioning
    override func startInteractiveTransition(_ transitionContext: UIViewControllerContextTransitioning) {
        self.transitionContext = transitionContext
        let fromViewController = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.from)
        if let view = fromViewController?.view {
            transitionContext.containerView.bringSubview(toFront: view)
        }
    }
}

extension ModalInteractiveTransition: UIViewControllerTransitioningDelegate {
    func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
        isDismiss = false
        return self
    }
    
    func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
        isDismiss = true
        return self
    }
    
    func interactionControllerForPresentation(using animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {
        return nil
    }
    
    func interactionControllerForDismissal(using animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {
        return self
    }
}

extension ModalInteractiveTransition: UIViewControllerAnimatedTransitioning {
    func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
        return ModalInteractiveTransitionConfiguration.transitionDuration
    }
    
    func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
        if isInteractionInProgress {
            return
        }
        
        guard let fromViewController = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.from),
            let toViewController = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.to),
            let fromView = fromViewController.view,
            let toView = toViewController.view
            else {
                return
        }
        
        self.transitionContext = transitionContext
        let containerView = transitionContext.containerView
        
        
        if isDismiss {
            // Animate dismiss
            if fromViewController.modalPresentationStyle == .fullScreen {
                containerView.addSubview(toView)
            }
            containerView.bringSubview(toFront: toView)
            
            var endRect = CGRect(x: 0,
                                 y: fromView.bounds.height,
                                 width: fromView.frame.width,
                                 height: fromView.frame.height)
            let transformedPoint = endRect.origin.applying(fromView.transform)
            endRect = CGRect(x: transformedPoint.x,
                             y: transformedPoint.y,
                             width: endRect.size.width,
                             height: endRect.size.height)
            
            if fromViewController.modalPresentationStyle == .custom {
                toViewController.beginAppearanceTransition(false, animated: true)
            }
            
            animate(animations: {
                fromView.frame = endRect
            }, completion: { _ in
                if fromViewController.modalPresentationStyle == .custom {
                    toViewController.endAppearanceTransition()
                }
                transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
            })
        } else {
            // Animate present
            containerView.addSubview(toView)
            // Present from bottom or from top
            let startY = configuration == .presentFromBottom
                ? containerView.bounds.height
                : -containerView.bounds.height
            let startRect = CGRect(x: 0,
                                   y: startY,
                                   width: containerView.bounds.width,
                                   height: containerView.bounds.height)
            
            let transformedPoint = startRect.origin.applying(toView.transform)
            toView.frame = CGRect(x: transformedPoint.x,
                                  y: transformedPoint.y,
                                  width: startRect.size.width,
                                  height: startRect.size.height)
            
            if toViewController.modalPresentationStyle == .custom {
                fromViewController.beginAppearanceTransition(false, animated: true)
            }
            
            animate(animations: {
                toView.frame = CGRect(x: 0,
                                      y: 0,
                                      width: toView.frame.width,
                                      height: toView.frame.height)
            }, completion: { _ in
                if toViewController.modalPresentationStyle == .custom {
                    fromViewController.endAppearanceTransition()
                }
                transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
            })
        }
    }
}