Skip to content

Instantly share code, notes, and snippets.

@christianselig
Created February 19, 2021 20:36
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save christianselig/400845c3b9171bfb3bed679506804a8d to your computer and use it in GitHub Desktop.
Save christianselig/400845c3b9171bfb3bed679506804a8d to your computer and use it in GitHub Desktop.
Interactive menu view controller transition that is interruptible, using *new school* UIViewPropertyAnimator
// 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)
let interruptableAnimator = interruptibleAnimator(using: transitionContext)
timingCurve = (interruptableAnimator as! UIViewPropertyAnimator).timingParameters
}
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 {
interruptibleAnimator(using: self.transitionContext!).fractionComplete = progress
update(progress)
}
}
func interruptibleAnimator(using transitionContext: UIViewControllerContextTransitioning) -> UIViewImplicitlyAnimating {
if let animatorForCurrentSession = animatorForCurrentSession {
return animatorForCurrentSession
}
let propertyAnimator = UIViewPropertyAnimator(duration: transitionDuration(using: transitionContext), dampingRatio: 0.75)
propertyAnimator.isInterruptible = true
propertyAnimator.isUserInteractionEnabled = true
let isPresenting = type == .presentation
guard let chidoriMenu: ChidoriMenu = {
return (isPresenting ? transitionContext.viewController(forKey: UITransitionContextViewControllerKey.to) : transitionContext.viewController(forKey: UITransitionContextViewControllerKey.from)) as? ChidoriMenu
}() else {
preconditionFailure("Menu should be accessible")
}
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! 🪄
propertyAnimator.addAnimations {
chidoriMenu.view.transform = finalTransform
chidoriMenu.view.alpha = finalAlpha
}
propertyAnimator.addCompletion { (position) in
print("Finish reached with position \(position) and \(position.rawValue) end: \(position == .end), start: \(position == .start), current: \(position == .current)")
if position == .start {
transitionContext.completeTransition(false)
self.animatorForCurrentSession = nil
return
}
guard position == .end else { return }
print("Got to end. Sus.")
// finish() should only be called once you're *sure* the animation is to end, in this case we know it will as the animation ended
self.finish()
transitionContext.completeTransition(true)
self.animatorForCurrentSession = nil
}
self.animatorForCurrentSession = propertyAnimator
return propertyAnimator
}
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