For boilerplate on setting up a gesture recognizer and such for the interactive transition, see this answer.
I am experimenting with interactive transitions, and spent quite a bit of time trying to figure out why the controllers would transition normally instead of scrubbing through according to the gesture. I discovered that it was not working because I am using a UIViewPropertyAnimator
. Switching to the older UIView animation blocks work out of the box. Why? What is the difference in implementation?
func animateTransition(using transitionContext: UIViewControllerContextTransitioning)
{
// Ignore the forced unwrapping, for sake of brevity.
let view_From = transitionContext.viewController(forKey: .from)!.view!
let view_To = transitionContext.viewController(forKey: .to)!.view!
transitionContext.containerView.insertSubview(view_To, aboveSubview: view_From)
view_To.alpha = 0
// This animation block works - it will follow the progress value of the interaction controller
UIView.animate(withDuration: 1, animations: {
view_From.alpha = 0.0
view_To.alpha = 1.0
}, completion: { finished in
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
})
// This animation block fails - it will play out normally and not be interactive
/*
let animator = UIViewPropertyAnimator(duration: 1, curve: .linear)
animator.addAnimations {
view_To.alpha = 1
view_From.alpha = 0
}
animator.addCompletion { (position) in
switch position {
case .end: print("Completion handler called at end of animation")
case .current: print("Completion handler called mid-way through animation")
case .start: print("Completion handler called at start of animation")
}
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
}
animator.startAnimation()
*/
}
With introduction of the UIViewPropertyAnimator
in iOS 10 the UIViewControllerAnimatedTransitioning
protocol got updated, too. They've added an optional func interruptibleAnimator(using: UIViewControllerContextTransitioning)
that you don't have to implement (I guess for backward compatibility). But it was added exactly for the use case you mention here: to take advantage of the new UIViewPropertyAnimator
.
So to get what you want: first, you have to implement interruptibleAnimator(using:)
to create the animator - you don't create it in animateTransition(using:)
.
As per comment in the source code of UIViewControllerAnimatedTransitioning
(emphasis is mine)(I have no idea why the documentation does not contain this info):
A conforming object implements this method if the transition it creates can be interrupted. For example, it could return an instance of a UIViewPropertyAnimator. It is expected that this method will return the same instance for the life of a transition.
You have to return the same animator for the duration of the transition. That's why you will find
private var animatorForCurrentSession: UIViewImplicitlyAnimating?
property in my BackAnimator
implementation - I store the current animator there to return it if the transition haven't ended.
When the interruptibleAnimator(using:)
is implemented, the environment will take that animator and use it instead of animating using animateTransition(using:)
. But to keep the contract of the protocol, animateTransition(using:)
should be able to animate the transition - but you can simply use the interruptibleAnimator(using:)
to create an animator and run the animation there.
Following is a working BackAnimator
implementation that you can use with the example you referred in this SO question. I used your code as basis, but you can simply swap my BackAnimator
for their implementation and you are good to go (I was testing it on their example).
class BackAnimator : NSObject, UIViewControllerAnimatedTransitioning {
// property for keeping the animator for current ongoing transition
private var animatorForCurrentTransition: UIViewImplicitlyAnimating?
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return 0.5
}
func interruptibleAnimator(using transitionContext: UIViewControllerContextTransitioning) -> UIViewImplicitlyAnimating {
// as per documentation, the same object should be returned for the ongoing transition
if let animatorForCurrentSession = animatorForCurrentTransition {
return animatorForCurrentSession
}
// normal creation of the propertyAnimator
let view_From = transitionContext.viewController(forKey: .from)!.view!
let view_To = transitionContext.viewController(forKey: .to)!.view!
transitionContext.containerView.insertSubview(view_To, aboveSubview: view_From)
view_To.alpha = 0
let animator = UIViewPropertyAnimator(duration: transitionDuration(using: transitionContext), curve: .linear)
animator.addAnimations {
view_To.alpha = 1
view_From.alpha = 0
}
animator.addCompletion { (position) in
switch position {
case .end: print("Completion handler called at end of animation")
case .current: print("Completion handler called mid-way through animation")
case .start: print("Completion handler called at start of animation")
}
// transition completed, reset the current animator:
self.animatorForCurrentTransition = nil
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
}
// keep the reference to current animator
self.animatorForCurrentTransition = animator
return animator
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
// animateTransition should work too, so let's just use the interruptibleAnimator implementation to achieve it
let anim = self.interruptibleAnimator(using: transitionContext)
anim.startAnimation()
}
}
Also notice that the animator returned by the interruptibleAnimator(using:)
is not started by us - the environment will start it when appropriate.
P.S.: Most of my knowledge on the subject comes from trying to implement an open source container that would allow custom interactive transitions between its containees - InteractiveTransitioningContainer. Maybe you'll find there some inspiration, too :).
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