Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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.

like image 855
Nico Avatar asked Jul 17 '15 00:07

Nico


3 Answers

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.

like image 88
Rob Avatar answered Oct 19 '22 08:10

Rob


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.

like image 25
Sam Xu Avatar answered Oct 19 '22 09:10

Sam Xu


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(...)
  }
}
like image 1
Steven McCracken Avatar answered Oct 19 '22 07:10

Steven McCracken