Interactive menu view controller transition that is interruptible, using *old school* UIView.animate APIs Raw
// MARK: - Animation Controller | |
class ChidoriAnimationController: UIPercentDrivenInteractiveTransition, UIViewControllerAnimatedTransitioning { | |
enum AnimationControllerType { case presentation, dismissal } | |
let type: AnimationControllerType | |
var animatorForCurrentSession: UIViewPropertyAnimator? | |
var transitionContext: UIViewControllerContextTransitioning? | |
var presentationAnimationTimeStart: CFTimeInterval? | |
init(type: AnimationControllerType) { | |
self.type = type | |
super.init() | |
} | |
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval { | |
return 5.0 | |
} | |
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {} | |
var displayLink: CADisplayLink? | |
override func startInteractiveTransition(_ transitionContext: UIViewControllerContextTransitioning) { | |
super.startInteractiveTransition(transitionContext) | |
self.transitionContext = transitionContext | |
guard let presentingViewController = transitionContext.viewController(forKey: type == .presentation ? .from : .to) else { | |
assertionFailure("Presenting view controller should be available") | |
return | |
} | |
if type == .presentation { | |
presentingViewController.view.tintAdjustmentMode = .dimmed | |
if let chidoriMenu: ChidoriMenu = transitionContext.viewController(forKey: .to) as? ChidoriMenu { | |
transitionContext.containerView.addSubview(chidoriMenu.view) | |
} | |
} | |
self.presentationAnimationTimeStart = CACurrentMediaTime() | |
let displayLink = CADisplayLink(target: self, selector: #selector(displayLinkUpdate(displayLink:))) | |
self.displayLink = displayLink | |
displayLink.add(to: .current, forMode: .common) | |
guard let chidoriMenu: ChidoriMenu = { | |
return (type == .presentation ? transitionContext.viewController(forKey: .to) : transitionContext.viewController(forKey: .from)) as? ChidoriMenu | |
}() else { | |
preconditionFailure("Menu should be accessible") | |
} | |
let isPresenting = type == .presentation | |
let finalFrame = transitionContext.finalFrame(for: chidoriMenu) | |
chidoriMenu.view.frame = finalFrame | |
// Rather than moving the origin of the view's frame for the animation (which is causing issues with jumpiness), just translate the view temporarily. | |
// Accomplish this by finding out how far we have to translate it by creating a reference point from the center of the menu we're moving, and compare that to the center point of where we're moving it to (we're moving it to a specific coordinate, not a frame, so the center point is the same as the coordinate) | |
let translationRequired = calculateTranslationRequired(forChidoriMenuFrame: finalFrame, toDesiredPoint: chidoriMenu.summonPoint) | |
let initialAlpha: CGFloat = isPresenting ? 0.0 : 1.0 | |
let finalAlpha: CGFloat = isPresenting ? 1.0 : 0.0 | |
let translatedAndScaledTransform = CGAffineTransform(translationX: translationRequired.dx, y: translationRequired.dy).scaledBy(x: 0.05, y: 0.05) | |
let initialTransform = isPresenting ? translatedAndScaledTransform : .identity | |
let finalTransform = isPresenting ? .identity : translatedAndScaledTransform | |
chidoriMenu.view.transform = initialTransform | |
chidoriMenu.view.alpha = initialAlpha | |
// Animate! 🪄 | |
UIView.animate(withDuration: transitionDuration(using: transitionContext), delay: 0.0, usingSpringWithDamping: 0.75, initialSpringVelocity: 0, options: [.allowUserInteraction, .beginFromCurrentState]) { | |
chidoriMenu.view.transform = finalTransform | |
chidoriMenu.view.alpha = finalAlpha | |
} completion: { (didComplete) in | |
if (isPresenting && transitionContext.transitionWasCancelled) || (!isPresenting && !transitionContext.transitionWasCancelled) { | |
presentingViewController.view.tintAdjustmentMode = .automatic | |
} | |
transitionContext.completeTransition(!transitionContext.transitionWasCancelled) | |
} | |
} | |
func cancelPresentationTransmission() { | |
displayLink?.invalidate() | |
self.cancel() | |
} | |
@objc private func displayLinkUpdate(displayLink: CADisplayLink) { | |
guard let presentationAnimationTimeStart = presentationAnimationTimeStart, let transitionContext = transitionContext else { | |
preconditionFailure("Should not start without a starting reference point or transition context") | |
} | |
let timeSinceAnimationBegan = displayLink.timestamp - presentationAnimationTimeStart | |
let progress = CGFloat(timeSinceAnimationBegan / transitionDuration(using: transitionContext)) | |
if progress >= 1.0 { | |
displayLink.invalidate() | |
finish() | |
self.animatorForCurrentSession = nil | |
} else { | |
update(progress) | |
} | |
} | |
private func calculateTranslationRequired(forChidoriMenuFrame chidoriMenuFrame: CGRect, toDesiredPoint desiredPoint: CGPoint) -> CGVector { | |
let centerPointOfMenuView = CGPoint(x: chidoriMenuFrame.origin.x + (chidoriMenuFrame.width / 2.0), y: chidoriMenuFrame.origin.y + (chidoriMenuFrame.height / 2.0)) | |
let translationRequired = CGVector(dx: desiredPoint.x - centerPointOfMenuView.x, dy: desiredPoint.y - centerPointOfMenuView.y) | |
return translationRequired | |
} | |
} | |
// Also added the following to `ChidoriMenu` itself: | |
let animationPresentationController = ChidoriAnimationController(type: .presentation) | |
let animationDismissalController = ChidoriAnimationController(type: .dismissal) | |
override func dismiss(animated flag: Bool, completion: (() -> Void)? = nil) { | |
if isBeingPresented { | |
animationPresentationController.cancelPresentationTransmission() | |
} else { | |
super.dismiss(animated: flag, completion: completion) | |
} | |
} | |
extension ChidoriMenu: UIViewControllerTransitioningDelegate { | |
func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? { | |
return animationPresentationController | |
} | |
func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? { | |
return animationDismissalController | |
} | |
func presentationController(forPresented presented: UIViewController, presenting: UIViewController?, source: UIViewController) -> UIPresentationController? { | |
return ChidoriPresentationController(presentedViewController: presented, presenting: presenting) | |
} | |
func interactionControllerForPresentation(using animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? { | |
return animationPresentationController | |
} | |
func interactionControllerForDismissal(using animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? { | |
return animationDismissalController | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment