[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)
})
}
}
}