In my app I'm dismissing a viewController using a UIPercentDrivenInteractiveTransition triggered by a pan gesture. I'm expecting my viewController to be dragged to the right as I'm panning it. However when I slowly pan I get a glitch: the viewController quickly jumps from left to right a bit. Here's the code for the transition:
class FilterHideTransition: UIPercentDrivenInteractiveTransition {
    let viewController: FilterViewController
    var enabled = false
    private let panGesture = UIPanGestureRecognizer()
    private let tapGesture = UITapGestureRecognizer()
    init(viewController: FilterViewController) {
        self.viewController = viewController
        super.init()
        panGesture.addTarget(self, action: #selector(didPan(with:)))
        panGesture.cancelsTouchesInView = false
        panGesture.delegate = self
        tapGesture.addTarget(self, action: #selector(didTap(with:)))
        tapGesture.cancelsTouchesInView = false
        tapGesture.delegate = self
        viewController.view.addGestureRecognizer(panGesture)
        viewController.view.addGestureRecognizer(tapGesture)
    }
}
//MARK: - Actions
private extension FilterHideTransition {
    @objc func didPan(with recognizer: UIPanGestureRecognizer) {
        let translation = recognizer.translation(in: viewController.view)
        let percentage = translation.x / viewController.view.frame.size.width
        print(percentage)
        switch recognizer.state {
        case .began:
            enabled = true
            viewController.dismiss(animated: true, completion: nil)
            break
        case .changed:
            update(percentage)
            break
        case .ended:
            completionSpeed = 0.3
            if percentage > 0.5 {
                finish()
            } else {
                cancel()
            }
            enabled = false
            break
        case .cancelled:
            cancel()
            enabled = false
            break
        default:
            cancel()
            enabled = false
            break
        }
    }
    @objc func didTap(with recognizer: UITapGestureRecognizer) {
        viewController.dismiss(animated: true, completion: nil)
    }
    func isTouch(touch: UITouch, in view: UIView) -> Bool {
        let touchPoint = touch.location(in: view)
        return view.hitTest(touchPoint, with: nil) != nil
    }
}
extension FilterHideTransition: UIGestureRecognizerDelegate {
    func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool {
        if gestureRecognizer == tapGesture {
            return !isTouch(touch: touch, in: viewController.panel)
        } else if gestureRecognizer == panGesture {
            return  !isTouch(touch: touch, in: viewController.heightSlider) &&
                !isTouch(touch: touch, in: viewController.widthSlider) &&
                !isTouch(touch: touch, in: viewController.priceSlider)
        } else {
            return true
        }
    }
}
Here's the code for the animator:
class FilterHideAnimator: NSObject, UIViewControllerAnimatedTransitioning {
    func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
        return 0.25
    }
    func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
        guard let fromVC = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.from),
              let toVC = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.to)  as? OverlayTabBarController
              else { return }
        let startFrame = fromVC.view.frame
        let endFrame = CGRect(x: startFrame.size.width, y: 0, width: startFrame.size.width, height: startFrame.size.height)
        UIView.animate(withDuration: transitionDuration(using: transitionContext),
                   delay: 0.0,
                   options: .curveEaseIn,
                   animations: {
                        fromVC.view.frame = endFrame
                        toVC.overlay.alpha = 0
                    },
                   completion: {
                        _ in
                        if transitionContext.transitionWasCancelled {
                            transitionContext.completeTransition(false)
                        } else {
                            transitionContext.completeTransition(true)
                        }
                    })
    }
}
My question: How can I prevent this glitch from happening?
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.
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