Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Glitch when interactively dismiss a modal view controller

I am using UIViewControllerAnimatedTransitioning and UIPercentDrivenInteractiveTransition to interactively dismiss a modally presented view controller. Nothing too fancy. But I noticed there is occasionally a small glitch just when the interaction starts. It becomes more noticeable if it is animated with .curveEaseOut option. Same thing happens with some online tutorial I am following (https://www.thorntech.com/2016/02/ios-tutorial-close-modal-dragging/). You can see the glitch in the gif when I drag it down for the first time. Any suggestions?

enter image description here

MyDismissAnimator

class SlideInDismissAnimator: NSObject, UIViewControllerAnimatedTransitioning {

    // ...

    func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {

        guard let toVC = transitionContext.viewController(forKey: .to), 
        let presentedVC = transitionContext.viewController(forKey: .from) else {return}

        let presentedFrame = transitionContext.finalFrame(for: presentedVC)
        var dismissedFrame = presentedFrame
        dismissedFrame.origin.y = transitionContext.containerView.frame.size.height

        transitionContext.containerView.insertSubview(toVC.view, belowSubview: presentedVC.view)
        }
        let duration = transitionDuration(using: transitionContext)

        UIView.animate(withDuration: duration, delay: 0, options: .curveEaseOut, animations: {
            presentedVC.view.frame = dismissedFrame
        }) { _ in
            if transitionContext.transitionWasCancelled {
                toVC.view.removeFromSuperview()
            }
            transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
        }
    }
}

MyInteractor

class SwipeInteractionController: UIPercentDrivenInteractiveTransition {

    var interactionInProgress = false
    private var shouldCompleteTransition = false
    private weak var viewController: UIViewController!

    init(viewController: UIViewController) {
        self.viewController = viewController
        super.init()
        let gesture = UIPanGestureRecognizer(target: self, action: #selector(handleGesture(_:)))
        viewController.view?.addGestureRecognizer(gesture)
    }

    @objc func handleGesture(_ gestureRecognizer: UIPanGestureRecognizer) {
        let translation = gestureRecognizer.translation(in: gestureRecognizer.view!.superview!)
        var progress = (translation.y / viewController.view.bounds.height)
        progress = CGFloat(fminf(fmaxf(Float(progress), 0.0), 1.0))

        switch gestureRecognizer.state {
        case .began:
            interactionInProgress = true
            viewController.dismiss(animated: true, completion: nil)

        case .changed:
            shouldCompleteTransition = progress > 0.3
            update(progress)

        case .cancelled:
            interactionInProgress = false
            cancel()

        case .ended:
            interactionInProgress = false
            if shouldCompleteTransition {
                finish()
            } else {
                cancel()
            }
        default:
            break
        }
    }
}
like image 236
Jack Guo Avatar asked Feb 07 '18 14:02

Jack Guo


2 Answers

I'm pretty sure this is a bug with the UIPercentDrivenInteractiveTransition, but I was able to resolve this:

The bug occurs when the progress is not updated as soon as possible after dismiss() is called. This happens because the pan gesture recognizer's .changed state is only triggered as you're dragging. So if you're dragging slowly, between the time .begin is called and the first .changed is called, the dismiss transition will start to animate.

You can see this in the simulator by very slowly dragging on the view, pixel by pixel, until .begin is called. As long as .changed doesn't get called again, the transition will actually complete itself and you'll see the view animate all the way down and transition complete.

However, simply calling update(progress) after .dismiss() doesn't work either, I believe because the dismiss animation hasn't begun yet. (until the next runloop or something)

My "hack" solution was to dispatch async with a very tiny amount of time, effectively setting the progress and halting the animation before it begins.

case .began:
    interactionInProgress = true
    viewController.dismiss(animated: true, completion: nil)
    DispatchQueue.main.asyncAfter(deadline: .now() + 0.001) {
        self.update(progress)
    }
like image 175
flagoworld Avatar answered Nov 15 '22 00:11

flagoworld


For interative dissmiss, the animation delay must be greater than 0 on iOS 11.

ref: https://github.com/jonkykong/SideMenu/blob/master/Pod/Classes/SideMenuTransition.swift#L510

like image 26
masashi sutou Avatar answered Nov 14 '22 22:11

masashi sutou