Skip to content

Instantly share code, notes, and snippets.

@christianselig

christianselig/old-school.swift Secret

Created Feb 19, 2021
Embed
What would you like to do?
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