157 lines
6.7 KiB
Swift
157 lines
6.7 KiB
Swift
// Copyright © 2020 Metabolist. All rights reserved.
|
|
|
|
import UIKit
|
|
|
|
final class ZoomDismissalInteractionController: NSObject {
|
|
var transitionContext: UIViewControllerContextTransitioning?
|
|
var animator: UIViewControllerAnimatedTransitioning?
|
|
|
|
var fromReferenceViewFrame: CGRect?
|
|
var toReferenceViewFrame: CGRect?
|
|
|
|
// swiftlint:disable:next function_body_length
|
|
func didPanWith(gestureRecognizer: UIPanGestureRecognizer) {
|
|
guard let transitionContext = self.transitionContext,
|
|
let animator = self.animator as? ZoomAnimator,
|
|
let transitionView = animator.transitionView,
|
|
let fromVC = transitionContext.viewController(forKey: .from),
|
|
let toVC = transitionContext.viewController(forKey: .to),
|
|
let fromReferenceView = animator.fromDelegate?.referenceView(for: animator),
|
|
let fromReferenceViewFrame = self.fromReferenceViewFrame
|
|
else { return }
|
|
|
|
let toReferenceView = animator.toDelegate?.referenceView(for: animator)
|
|
|
|
fromReferenceView.isHidden = true
|
|
|
|
let anchorPoint = CGPoint(x: fromReferenceViewFrame.midX, y: fromReferenceViewFrame.midY)
|
|
let dismissThreshold = fromReferenceViewFrame.height / 8
|
|
let translatedPoint = gestureRecognizer.translation(in: fromReferenceView)
|
|
|
|
let backgroundAlpha = backgroundAlphaFor(view: fromVC.view, withPanningVerticalDelta: translatedPoint.y)
|
|
let scale = scaleFor(view: fromVC.view, withPanningVerticalDelta: translatedPoint.y)
|
|
|
|
fromVC.view.alpha = backgroundAlpha
|
|
|
|
transitionView.transform = CGAffineTransform(scaleX: scale, y: scale)
|
|
let newCenter = CGPoint(
|
|
x: anchorPoint.x + translatedPoint.x,
|
|
y: anchorPoint.y + translatedPoint.y - transitionView.frame.height * (1 - scale) / 2.0)
|
|
transitionView.center = newCenter
|
|
|
|
toReferenceView?.isHidden = true
|
|
|
|
transitionContext.updateInteractiveTransition(1 - scale)
|
|
|
|
toVC.tabBarController?.tabBar.alpha = 1 - backgroundAlpha
|
|
|
|
if gestureRecognizer.state == .ended {
|
|
if abs(anchorPoint.y - newCenter.y) < dismissThreshold {
|
|
// cancel
|
|
UIView.animate(
|
|
withDuration: 0.5,
|
|
delay: 0,
|
|
usingSpringWithDamping: 0.9,
|
|
initialSpringVelocity: 0,
|
|
options: []) {
|
|
transitionView.frame = fromReferenceViewFrame
|
|
fromVC.view.alpha = 1.0
|
|
toVC.tabBarController?.tabBar.alpha = 0
|
|
} completion: { _ in
|
|
toReferenceView?.isHidden = false
|
|
fromReferenceView.isHidden = false
|
|
transitionView.removeFromSuperview()
|
|
animator.transitionView = nil
|
|
transitionContext.cancelInteractiveTransition()
|
|
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
|
|
animator.toDelegate?.transitionDidEndWith(zoomAnimator: animator)
|
|
animator.fromDelegate?.transitionDidEndWith(zoomAnimator: animator)
|
|
self.transitionContext = nil
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
// start animation
|
|
UIView.animate(
|
|
withDuration: .shortAnimationDuration) {
|
|
fromVC.view.alpha = 0
|
|
|
|
if let toReferenceViewFrame = self.toReferenceViewFrame {
|
|
transitionView.frame = toReferenceViewFrame
|
|
} else {
|
|
transitionView.alpha = 0
|
|
}
|
|
|
|
transitionView.layer.contentsRect = toReferenceView?.layer.contentsRect ?? .defaultContentsRect
|
|
|
|
toVC.tabBarController?.tabBar.alpha = 1
|
|
} completion: { _ in
|
|
transitionView.removeFromSuperview()
|
|
toReferenceView?.isHidden = false
|
|
fromReferenceView.isHidden = false
|
|
|
|
self.transitionContext?.finishInteractiveTransition()
|
|
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
|
|
animator.toDelegate?.transitionDidEndWith(zoomAnimator: animator)
|
|
animator.fromDelegate?.transitionDidEndWith(zoomAnimator: animator)
|
|
self.transitionContext = nil
|
|
}
|
|
}
|
|
}
|
|
|
|
func backgroundAlphaFor(view: UIView, withPanningVerticalDelta verticalDelta: CGFloat) -> CGFloat {
|
|
let startingAlpha: CGFloat = 1.0
|
|
let finalAlpha: CGFloat = 0.0
|
|
let totalAvailableAlpha = startingAlpha - finalAlpha
|
|
|
|
let maximumDelta = view.bounds.height / 4.0
|
|
let deltaAsPercentageOfMaximun = min(abs(verticalDelta) / maximumDelta, 1.0)
|
|
|
|
return startingAlpha - (deltaAsPercentageOfMaximun * totalAvailableAlpha)
|
|
}
|
|
|
|
func scaleFor(view: UIView, withPanningVerticalDelta verticalDelta: CGFloat) -> CGFloat {
|
|
let startingScale: CGFloat = 1.0
|
|
let finalScale: CGFloat = 0.5
|
|
let totalAvailableScale = startingScale - finalScale
|
|
|
|
let maximumDelta = view.bounds.height / 2.0
|
|
let deltaAsPercentageOfMaximun = min(abs(verticalDelta) / maximumDelta, 1.0)
|
|
|
|
return startingScale - (deltaAsPercentageOfMaximun * totalAvailableScale)
|
|
}
|
|
}
|
|
|
|
extension ZoomDismissalInteractionController: UIViewControllerInteractiveTransitioning {
|
|
func startInteractiveTransition(_ transitionContext: UIViewControllerContextTransitioning) {
|
|
guard let animator = animator as? ZoomAnimator else { return }
|
|
|
|
animator.fromDelegate?.transitionWillStartWith(zoomAnimator: animator)
|
|
animator.toDelegate?.transitionWillStartWith(zoomAnimator: animator)
|
|
|
|
self.transitionContext = transitionContext
|
|
|
|
let containerView = transitionContext.containerView
|
|
|
|
guard
|
|
let fromVC = transitionContext.viewController(forKey: .from),
|
|
let toVC = transitionContext.viewController(forKey: .to),
|
|
let fromReferenceViewFrame = animator.fromDelegate?.referenceViewFrameInTransitioningView(for: animator),
|
|
let fromReferenceView = animator.fromDelegate?.referenceView(for: animator)
|
|
else { return }
|
|
|
|
self.fromReferenceViewFrame = fromReferenceViewFrame
|
|
toReferenceViewFrame = animator.toDelegate?.referenceViewFrameInTransitioningView(for: animator)
|
|
|
|
containerView.insertSubview(toVC.view, belowSubview: fromVC.view)
|
|
|
|
if animator.transitionView == nil,
|
|
let transitionView = (fromReferenceView as? ZoomAnimatableView)?.transitionView() {
|
|
transitionView.frame = fromReferenceViewFrame
|
|
animator.transitionView = transitionView
|
|
containerView.addSubview(transitionView)
|
|
}
|
|
}
|
|
}
|