I am using UIViewControllerAnimatedTransitioning
protocol
with an attached UIViewPropertyAnimator
to pan down to dismiss a View Controller
extension SecondViewController : UIViewControllerAnimatedTransitioning {
func interruptibleAnimator(using ctx: UIViewControllerContextTransitioning) -> UIViewImplicitlyAnimating {
if self.animator != nil {
return self.animator!
}
let containerView = ctx.containerView
let toVC = ctx.viewController(forKey: .to) as! FirstViewController
let fromVC = ctx.viewController(forKey: .from) as! SecondViewController
containerView.insertSubview(toVC.view, belowSubview: fromVC.view)
self.animator = UIViewPropertyAnimator(duration: transitionDuration(using: ctx),
curve: .easeOut, animations: {
self.fromVC.view.transform = CGAffineTransform(scale: 0.5)
})
self.animator.isInterruptible = true
self.animator.isUserInteractionEnabled = true
self.animator.isManualHitTestingEnabled = true
self.animator.addCompletion { position in
switch position {
case .end:
break
case .current:
break
case .start:
break
}
let cancelled = ctx.transitionWasCancelled
if (cancelled) {
//..
} else {
//..
}
ctx.completeTransition(!cancelled)
}
self.animator = anim
return self.animator
}
func transitionDuration(using ctx: UIViewControllerContextTransitioning?) -> TimeInterval {
return 0.5
}
func animateTransition(using ctx: UIViewControllerContextTransitioning) {
let animator = self.interruptibleAnimator(using: ctx)
self.animator.startAnimation()
}
func animationEnded(_ transitionCompleted: Bool) {
self.interactiveTransition = nil
self.animator = nil
}
}
Pan Gesture to handle the animation:
func handlePanGesture(gestureRecognizer: UIPanGestureRecognizer) {
let panTranslation = gestureRecognizer.translation(in: gestureRecognizer.view!)
var progress = panTranslation.y / (gestureRecognizer.view!.bounds.size.height * 0.5)
switch gestureRecognizer.state {
case .began:
self.interactiveTransition = UIPercentDrivenInteractiveTransition()
self.navigationController!.popViewController(animated: true)
case .changed:
self.interactiveTransition!.update(progress)
case .cancelled, .ended:
if progress > 0.5 {
//Complete Transition
let timingParameters = UICubicTimingParameters(animationCurve: .easeInOut)
self.animator!.continueAnimation!(withTimingParameters: timingParameters, durationFactor: progress)
self.animator?.addAnimations! {
//Completion Animations
}
self.interactiveTransition!.finish()
} else {
//Cancel Transition
self.animator!.isReversed = true
let timingParameters = UICubicTimingParameters(animationCurve: .easeInOut)
self.animator!.continueAnimation!(withTimingParameters: timingParameters, durationFactor: progress)
self.animator!.addAnimations!({
//Cancelling Animations
}, delayFactor: 0 )
self.interactiveTransition!.cancel()
}
default:
break
}
}
Swiping down to dismissal works perfectly. Swiping slightly down and lifting finger to cancel also works perfectly.
Swiping down and back up beyond starting point (where progress becomes negative) and lifting up the finger should cancel the transition with cancelling animation. This happens in iOS 10 but it first reverses the navigation controller transitions first, then snaps back. In iOS 11, cancelling animation happens, then I see navigation controller transition is reversed. If you wait, you can see navigation controller transition does try to correct it self in animation over 10 mins or so.
Issue with:
- self.interactiveTransition!.cancel()?
- self.interactiveTransition!.completionSpeed ??
I don't know if this is a bug or we're all just doing it wrong but to correct the behavior, add .completionSpeed = 0.999
to the interactionController
in the .ended
case of the pan gesture handler. It's a hack but at least it's only a single line.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With