Slowly panning in UIPercentDrivenInteractiveTransition results in glitch

I tested your minimal working example and the same issue reappears. I wasn't able to fix it using UIView.animate API, but the issue does not appear if you use UIViewPropertyAnimator - only drawback is that UIViewPropertyAnimator is available only from iOS 10+.

iOS 10+ SOLUTION

First refactor HideAnimator to implement interruptibleAnimator(using:) to return a UIViewPropertyAnimator object that performs the transition animator (note that as per documentation we are supposed to return the same animator object for ongoing transition):

class HideAnimator: NSObject, UIViewControllerAnimatedTransitioning {

    fileprivate var propertyAnimator: UIViewPropertyAnimator?

    func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
        return 0.25
    }

    func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
        // use animator to implement animateTransition
        let animator = interruptibleAnimator(using: transitionContext)
        animator.startAnimation()
    }

    func interruptibleAnimator(using transitionContext: UIViewControllerContextTransitioning) -> UIViewImplicitlyAnimating {
        // as per documentation, we need to return existing animator
        // for ongoing transition
        if let propertyAnimator = propertyAnimator {
            return propertyAnimator
        }

        guard let fromVC = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.from)
            else { fatalError() }

        let startFrame = fromVC.view.frame
        let endFrame = CGRect(x: startFrame.size.width, y: 0, width: startFrame.size.width, height: startFrame.size.height)

        let animator = UIViewPropertyAnimator(duration: transitionDuration(using: transitionContext), timingParameters: UICubicTimingParameters(animationCurve: .easeInOut))
        animator.addAnimations {
            fromVC.view.frame = endFrame
        }
        animator.addCompletion { (_) in
            if transitionContext.transitionWasCancelled {
                transitionContext.completeTransition(false)
            } else {
                transitionContext.completeTransition(true)
            }
            // reset animator because the current transition ended
            self.propertyAnimator = nil
        }
        self.propertyAnimator = animator
        return animator
    }
}

One last thing to make it work, in didPan(with:) remove following line:

completionSpeed = 0.3

This will use the default speed (which is 1.0, or you can set it explicitly). When using interruptibleAnimator(using:) the completion speed is automatically calculated based on the fractionComplete of the animator.


So the issue is actually that when you initiate an interactive transition, the animation tries to run in its entirety. If you put a breakpoint in the gestures change state, you can see the entire animation run, and when you resume, it picks back up tracking your finger. I tried a bunch of hacks around setting the interactive transition's progress to 0 but nothing seemed to work.

The solution involves setting the transition context's container view's layer speed to 0 during the transition and setting it back to 1 when the transition is ready to complete. I abstracted this code into a simple subclass of UIPercentDrivenInteractiveTransition. The code looks something like:

@implementation InteractiveTransition {
  id<UIViewControllerContextTransitioning> _transitionContext;
}

- (void)startInteractiveTransition:(id<UIViewControllerContextTransitioning>)transitionContext {
  _transitionContext = transitionContext;
  [_transitionContext containerView].layer.speed = 0;
  [super startInteractiveTransition:transitionContext];
}

- (void)finishInteractiveTransition {
  [_transitionContext containerView].layer.speed = 1;
  [super finishInteractiveTransition];
}

- (void)cancelInteractiveTransition {
  [_transitionContext containerView].layer.speed = 1;
  [super cancelInteractiveTransition];
}

@end

This will pause the animation until you're ready to finish or cancel the interactive transition.