UIPercentDrivenInteractiveTransition doesn't get to animation's completion on fast gesture

I have created an interactive transition. My func animateTransition(transitionContext: UIViewControllerContextTransitioning) is quite normal, I get the container UIView, I add the two UIViewControllers and then I do the animation changes in a UIView.animateWithDuration(duration, animations, completion).

I add a UIScreenEdgePanGestureRecognizer to my from UIViewController. It works well except when I do a very quick pan.

In that last scenario, the app is not responsive, still on the same UIViewController (the transition seems not to have worked) but the background tasks run. When I run the Debug View Hierarchy, I see the new UIViewController instead of the previous one, and the previous one (at least its UIView) stands where it is supposed to stand at the end of the transition.

I did some print out and check points and from that I can say that when the problem occurs, the animation's completion (the one in my animateTransition method) is not reached, so I cannot call the transitionContext.completeTransition method to complete or not the transition.

I could see as well that the pan goes sometimes from UIGestureRecognizerState.Began straight to UIGestureRecognizerState.Ended without going through UIGestureRecognizerState.Changed.

When it goes through UIGestureRecognizerState.Changed, both the translation and the velocity stay the same for every UIGestureRecognizerState.Changed states.

EDIT :

Here is the code:

animateTransition method

func animateTransition(transitionContext: UIViewControllerContextTransitioning) {
    self.transitionContext = transitionContext

    let containerView = transitionContext.containerView()
    let screens: (from: UIViewController, to: UIViewController) = (transitionContext.viewControllerForKey(UITransitionContextFromViewControllerKey)!, transitionContext.viewControllerForKey(UITransitionContextToViewControllerKey)!)

    let parentViewController = presenting ? screens.from : screens.to
    let childViewController = presenting ? screens.to : screens.from

    let parentView = parentViewController.view
    let childView = childViewController.view

    // positionning the "to" viewController's view for the animation
    if presenting {
        offStageChildViewController(childView)
    }

    containerView.addSubview(parentView)    
    containerView.addSubview(childView)

    let duration = transitionDuration(transitionContext)

    UIView.animateWithDuration(duration, animations: {

        if self.presenting {
            self.onStageViewController(childView)
            self.offStageParentViewController(parentView)
        } else {
            self.onStageViewController(parentView)
            self.offStageChildViewController(childView)
        }}, completion: { finished in
            if transitionContext.transitionWasCancelled() {
                transitionContext.completeTransition(false)
            } else {
                transitionContext.completeTransition(true)
            }
    })
}

Gesture and gesture handler:

weak var fromViewController: UIViewController! {
    didSet {
        let screenEdgePanRecognizer = UIScreenEdgePanGestureRecognizer(target: self, action: "presentingViewController:")
        screenEdgePanRecognizer.edges = edge
        fromViewController.view.addGestureRecognizer(screenEdgePanRecognizer)
    }
}

func presentingViewController(pan: UIPanGestureRecognizer) {
    let percentage = getPercentage(pan)

    switch pan.state {

    case UIGestureRecognizerState.Began:
        interactive = true
        presentViewController(pan)

    case UIGestureRecognizerState.Changed:
        updateInteractiveTransition(percentage)

    case UIGestureRecognizerState.Ended:
        interactive = false

        if finishPresenting(pan, percentage: percentage) {
            finishInteractiveTransition()
        } else {
            cancelInteractiveTransition()
        }

    default:
        break
    }
}

Any idea what might happen?

EDIT 2:

Here are the undisclosed methods:

override func getPercentage(pan: UIPanGestureRecognizer) -> CGFloat {
    let translation = pan.translationInView(pan.view!)
    return abs(translation.x / pan.view!.bounds.width)
}

override func onStageViewController(view: UIView) {
    view.transform = CGAffineTransformIdentity
}

override func offStageParentViewController(view: UIView) {
    view.transform = CGAffineTransformMakeTranslation(-view.bounds.width / 2, 0)
}

override func offStageChildViewController(view: UIView) {
    view.transform = CGAffineTransformMakeTranslation(view.bounds.width, 0)
}

override func presentViewController(pan: UIPanGestureRecognizer) {
    let location = pan.locationInView((fromViewController as! MainViewController).tableView)
    let indexPath = (fromViewController as! MainViewController).tableView.indexPathForRowAtPoint(location)

    if indexPath == nil {
        pan.state = .Failed
        return
    }
    fromViewController.performSegueWithIdentifier("chartSegue", sender: pan)
}
  • I remove the "over" adding lines => didn't fix it
  • I added updateInteractiveTransition in .Began, in .Ended, in both => didn't fix it
  • I turned on shouldRasterize on the layer of the view of my toViewController and let it on all the time => didn't fix it

But the question is why, when doing a fast interactive gesture, is it not responding quickly enough

It actually works with a fast interactive gesture as long as I leave my finger long enough. For example, if I pan very fast on more than (let say) 1cm, it's ok. It's not ok if I pan very fast on a small surface (let say again) less than 1cm

Possible candidates include the views being animated are too complicated (or have complicated effects like shading)

I thought about a complicated view as well but I don't think my view is really complicated. There are a bunch of buttons and labels, a custom UIControl acting as a segmented segment, a chart (that is loaded once the controller appeared) and a xib is loaded inside the viewController.


Ok I just created a project with the MINIMUM classes and objects in order to trigger the problem. So to trigger it, you just do a fast and brief swipe from the right to the left.

What I noticed is that it works pretty easily the first time but if you drag the view controller normally the first time, then it get much harder to trigger it (even impossible?). While in my full project, it doesn't really matter.

When I was diagnosing this problem, I noticed that the gesture's change and ended state events were taking place before animateTransition even ran. So the animation was canceled/finished before it even started!

I tried using GCD animation synchronization queue to ensure that the updating of the UIPercentDrivenInterativeTransition doesn't happen until after `animate:

private let animationSynchronizationQueue = dispatch_queue_create("com.domain.app.animationsynchronization", DISPATCH_QUEUE_SERIAL)

I then had a utility method to use this queue:

func dispatchToMainFromSynchronizationQueue(block: dispatch_block_t) {
    dispatch_async(animationSynchronizationQueue) {
        dispatch_sync(dispatch_get_main_queue(), block)
    }
}

And then my gesture handler made sure that changes and ended states were routed through that queue:

func handlePan(gesture: UIPanGestureRecognizer) {
    switch gesture.state {

    case .Began:
        dispatch_suspend(animationSynchronizationQueue)
        fromViewController.performSegueWithIdentifier("segueID", sender: gesture)

    case .Changed:
        dispatchToMainFromSynchronizationQueue() {
            self.updateInteractiveTransition(percentage)
        }

    case .Ended:
        dispatchToMainFromSynchronizationQueue() {
            if isOkToFinish {
                self.finishInteractiveTransition()
            } else {
                self.cancelInteractiveTransition()
            }
        }

    default:
        break
    }
}

So, I have the gesture recognizer's .Began state suspend that queue, and I have the animation controller resume that queue in animationTransition (ensuring that the queue starts again only after that method runs before the gesture proceeds to try to update the UIPercentDrivenInteractiveTransition object.

ios, When I was diagnosing this problem, I noticed that the gesture's change and ended state events were taking place before animateTransition� Teams. Q&A for Work. Stack Overflow for Teams is a private, secure spot for you and your coworkers to find and share information.

Have the same issue, tried to use serialQueue.suspend()/resume(), does not work.

This issue is because when pan gesture is too fast, end state is earlier than animateTransition starts, then context.completeTransition can not get run, the whole animation is messed up.

My solution is forcing to run context.completeTransition when this situation happened.

For example, I have two classes:

class SwipeInteractor: UIPercentDrivenInteractiveTransition {
    var interactionInProgress = false

    ...
}

class AnimationController: UIViewControllerAnimatedTransitioning {
    func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
        if !swipeInteractor.interactionInProgress {
            DispatchQueue.main.asyncAfter(deadline: .now()+transitionDuration) {
                if context.transitionWasCancelled {
                    toView?.removeFromSuperview()
                } else {
                    fromView?.removeFromSuperview()
                }
                context.completeTransition(!context.transitionWasCancelled)
            }
        }

        ...
    }

    ...
}

interactionInProgress is set to true when gesture began, set to false when gesture ends.

Xcode 10 Sim: Problem with Custom …, a `UIPercentDrivenInteractiveTransition`, call `dismiss(animated:completion:)` I should also make sure the transition delegate doesn't attempt an interactive� Even if you’re some kind of nerd who doesn’t think that about every animation you see, you’ll get to learn a lot about view controller transition animations while exploring this animation. In this tutorial, you’ll learn how to implement this cool animation in Swift using a UIViewController transition animation .

I had a similar problem, but with programmatic animation triggers not triggering the animation completion block. My solution was like Sam's, except instead of dispatching after a small delay, manually call finish on the UIPercentDrivenInteractiveTransition instance.

class SwipeInteractor: UIPercentDrivenInteractiveTransition {
  var interactionInProgress = false
  ...
}

class AnimationController: UIViewControllerAnimatedTransitioning {
  private var swipeInteractor: SwipeInteractor
  ..
  func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
    ...
    if !swipeInteractor.interactionInProgress {
      swipeInteractor.finish()
    }
    ...
    UIView.animateWithDuration(...)
  }
}

38766932: UIPercentDrivenInteractiveTransition doesn't control , CAAnimation (even when wrapped on CATransaction), UIPercentDrivenInteractiveTransition does not control the animation interactively. Ste. Customizing the Transition Animations. Transition animations provide visual feedback about changes to your app’s interface. UIKit provides a set of standard transition styles to use when presenting view controllers, and you can supplement the standard transitions with custom transitions of your own.

Build a Custom Animated Transition between UIViewControllers, extend the UIPercentDrivenInteractiveTransition class. UIKit does the heavylifting for these animated transitions. It manages all the interactions� So far, it doesn’t look anything like a slide-out menu. It’s just a blue View Controller that opens a green modal. You can tap the buttons in the corners to open and close the modal. Here’s the code so far: MainViewController.swift; MenuViewController.swift; 2. Add some helpful files. Before diving in, you first need to create two Swift

Mastering view controller transitions, part 2: Make them feel natural , while it might seem like a simple UIPercentDrivenInteractiveTransition could get us Unfortunately, this doesn't end up feeling very natural:. The transition that we created for a regular transition doesn’t really make sense with our gesture. Lets change the transition animation so that it stays the same for a regular transition (by tapping on the + or cancel buttons) but has the menu items sliding across the screen for the either of the gestures.

Custom UIViewController Transitions: Getting Started , If it doesn't have one, UIKIt uses the standard, built-in transition. of UIPercentDrivenInteractiveTransition and set the language to Swift. One of the UX considerations Apple made with iOS 7 was giving users a swipe to go back mechanism. If you’re anything like me, an app that doesn’t implement it is nearly an instant uninstall

Comments
  • @Rob I tried everything but it didn't solve the problem. The only time when it was ok was when the ToViewController was almost empty. I updated the question with a link to a minimal project that trigger the problem. Thanks for your help.
  • @Rob did you try it on the simulator or on a device? For your information, on the simulator I cannot reproduce it at all even with the full project. I'm using my iphone 6 and the last version of iOS
  • okay thanks again for your time on this. I notice something else as well. If you try to present the toViewController and cancel the transition, the object is not deinitialized. It will be deinitialized the next time you present ANY view controller (even if you call the same view controller).
  • Let us continue this discussion in chat.
  • Thanks Rob, just a few questions. First, is there a way to know if dispatch_suspend(animationSynchronizationQueue) has been called? I'm using this animator for several transition and when I don't suspend the queue, I got an error when I resume it. Second, the velocity is equal to 0 when the code in dispatchToMainFromSynchronizationQueue is reached, is it normal?. Then I captured the velocity outside the queue to fix it but not sure it's the best way.
  • No, there's no way to know if it's suspended, so add your own Bool ivar to keep track of that. Re behavior when velocity is zero, no, dispatchToMainFromSynchronizationQueue doesn't change your velocity == 0 logic at all, so I suspect something else is going on.
  • Regarding the velocity, it seems that once .Ended has been reached and the code in it executed, the velocity is reseted. I could see that some code in .Changed (inside the animation synchronization queue) was called after .Ended had been reached and the value of the velocity was equal to 0.
  • This solution did not work for me alas. I ended up calling transitionContext.completeTransition() from the controller if the animation did not complete. So by saving the context, and waiting for the animation duration (in UIGestureRecognizerState.Ended) before calling completeTransition if it had not already been called. A very messy solution.
  • @sdsykes can you give a small code example where you're doing what exactly? I'm facing the exact same problem...