-
-
Save christianselig/400845c3b9171bfb3bed679506804a8d to your computer and use it in GitHub Desktop.
Interactive menu view controller transition that is interruptible, using *new school* UIViewPropertyAnimator
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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