UIPercentDrivenInteractiveTransition yielding extraneous animation when done
Asked Answered
S

3

21

I'm using an interactive custom push transition with a UIPercentDrivenInteractiveTransition. The gesture recognizer successfully calls the interaction controller's updateInteractiveTransition. Likewise, the animation successfully completes when I call the interaction controller's finishInteractiveTransition.

But, sometimes I get a extra bit of distracting animation at the end (where it seems to repeat the latter part of the animation). With reasonably simple animations, I rarely see this symptom on the iPhone 5 (though I routinely see it on the simulator when working on slow laptop). If I make the animation more computationally expensive (e.g. lots of shadows, multiple views animating different directions, etc.), the frequency of this problem on the device increases.

Has anyone else seen this problem and figured out a solution other than streamlining the animations (which I admittedly should do anyway) and/or writing my own interaction controllers? The UIPercentDrivenInteractiveTransition approach has a certain elegance to it, but I'm uneasy with the fact that it misbehaves non-deterministically. Have others seen this behavior? Does anyone know of other solutions?

To illustrate the effect, see the image below. Notice how the second scene, the red view, when the animation finishes, seems to repeat the latter part of its animation a second time.

animation not right

This animation is generated by:

  • repeatedly calling updateInteractiveTransition, progressing update from 0% to 40%;

  • momentarily pausing (so you can differentiate between the interactive transition and the completion animation resulting from finishInteractiveTransition);

  • then calling finishInteractiveTransition to complete the animation; and

  • the animation controller's animation's completion block calls completeTransition for the transitionContext, in order to clean everything up.

Doing some diagnostics, it appears that it is this last step that triggers that extraneous bit of animation. The animation controller's completion block is called when the animation is finished, but as soon as I call completeTransition, it sometimes repeats the last bit of the animation (notably when using complex animations).


I don't think it's relevant, but this is my code for configuring the navigation controller to perform interactive transitions:

- (void)viewDidLoad
{
    [super viewDidLoad];

    self.navigationController.delegate = self;

    self.interationController = [[UIPercentDrivenInteractiveTransition alloc] init];
}

- (id<UIViewControllerAnimatedTransitioning>)navigationController:(UINavigationController *)navigationController
                                  animationControllerForOperation:(UINavigationControllerOperation)operation
                                               fromViewController:(UIViewController*)fromVC
                                                 toViewController:(UIViewController*)toVC
{
    if (operation == UINavigationControllerOperationPush)
        return [[PushAnimator alloc] init];

    return nil;
}

- (id <UIViewControllerInteractiveTransitioning>)navigationController:(UINavigationController*)navigationController
                          interactionControllerForAnimationController:(id <UIViewControllerAnimatedTransitioning>)animationController
{
    return self.interationController;
}

My PushAnimator is:

@implementation PushAnimator

- (NSTimeInterval)transitionDuration:(id <UIViewControllerContextTransitioning>)transitionContext
{
    return 5.0;
}

- (void)animateTransition:(id<UIViewControllerContextTransitioning>)transitionContext
{
    UIViewController* toViewController   = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
    UIViewController* fromViewController = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];

    [[transitionContext containerView] addSubview:toViewController.view];
    toViewController.view.frame = CGRectOffset(fromViewController.view.frame, fromViewController.view.frame.size.width, 0);;

    [UIView animateWithDuration:[self transitionDuration:transitionContext] animations:^{
        toViewController.view.frame = fromViewController.view.frame;
    } completion:^(BOOL finished) {
        [transitionContext completeTransition:![transitionContext transitionWasCancelled]];
    }];
}

@end

Note, when I put logging statement where I call completeTransition, I can see that this extraneous bit of animation happens after I call completeTransition (even though the animation was really done at that point). This would suggest that that extra animation may have been a result of the call to completeTransition.

FYI, I've done this experiment with a gesture recognizer:

- (void)handlePan:(UIScreenEdgePanGestureRecognizer *)gesture
{
    CGFloat width = gesture.view.frame.size.width;

    if (gesture.state == UIGestureRecognizerStateBegan) {
        [self performSegueWithIdentifier:@"pushToSecond" sender:self];
    } else if (gesture.state == UIGestureRecognizerStateChanged) {
        CGPoint translation = [gesture translationInView:gesture.view];
        [self.interactionController updateInteractiveTransition:ABS(translation.x / width)];
    } else if (gesture.state == UIGestureRecognizerStateEnded ||
               gesture.state == UIGestureRecognizerStateCancelled)
    {
        CGPoint translation = [gesture translationInView:gesture.view];
        CGPoint velocity    = [gesture velocityInView:gesture.view];
        CGFloat percent     = ABS(translation.x + velocity.x * 0.25 / width);

        if (percent < 0.5 || gesture.state == UIGestureRecognizerStateCancelled) {
            [self.interactionController cancelInteractiveTransition];
        } else {
            [self.interactionController finishInteractiveTransition];
        }
    }
}

I also did it by calling the updateInteractiveTransition and finishInteractiveTransition manually (eliminating the gesture recognizer from the equation), and it still exhibits this strange behavior:

[self performSegueWithIdentifier:@"pushToSecond" sender:self];

dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
    [self.interactionController updateInteractiveTransition:0.40];

    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        [self.interactionController finishInteractiveTransition];
    });
});

Bottom line, I've concluded that this is a problem isolated to UIPercentDrivenInteractiveTransition with complex animations. I can minimize the problem by simplifying them (e.g. snapshotting and animated snapshotted views). I also suspect I could solve this by not using UIPercentDrivenInteractiveTransition and writing my own interaction controller, which would do the animation itself, without trying to interpolate the animationWithDuration block.

But I was wondering if anyone has figured out any other tricks to using UIPercentDrivenInteractiveTransition with complex animations.

Silk answered 13/4, 2014 at 19:24 Comment(7)
I reported something like this to Apple way back during iOS 7 beta process (like, August 2013). They closed it as being purely a simulator bug (and a duplicate, of bug 10535951). Can you actually reproduce it on the device?Monkeypot
By the way, a workaround might be to set the completionSpeed to 0.5 at the time the user interaction ends.Monkeypot
@Monkeypot Yes, I can reproduce it on a device. At first, like Apple, I dismissed this as a simulator bug, but when I made the animation more complicated (notably, when I add shadows to the "to" view controller's view and animated the "from" view controller's view at the same time), I noticed that I started experiencing it intermittently on my iPhone 5 (and I wager I might even see it even more on a slower device, such as iPhone 4). Admittedly, with short duration, it almost looks like an intermittent bounce effect, so I doubt my users would ever notice, but I do.Silk
BTW, I did try playing around with completionSpeed (and completionCurve) to no avail, but I'll try again.Silk
And please do file a bug, esp. since you have this good example that can be reproduced even on the device!Monkeypot
Done. Issue #16607330.Silk
possible duplicate of iOS 7 custom transition glitchVinavinaceous
M
6

I've seen something similar. I have two possible workarounds. One is to use delayed performance in the animation completion handler:

} completion:^(BOOL finished) {
        double delayInSeconds = 0.1;
        dispatch_time_t popTime = 
             dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC));
        dispatch_after(popTime, dispatch_get_main_queue(), ^(void){
            BOOL cancelled = [transitionContext transitionWasCancelled];
            [transitionContext completeTransition:!cancelled];
        });
       self.interacting = NO;
}];

The other possibility is: don't use percent-drive animation! I've never had a problem like this when driving the interactive custom animation myself manually.

Monkeypot answered 13/4, 2014 at 21:54 Comment(1)
Elegant in its simplicity. The delay fixed it on both simulator and device, though I'll probably retire the UIPercentDrivenInteractiveTransition altogether. Good to know that there is such an easy work-around, though.Silk
H
12

This problem arises only in simulator.

SOLUTION: self.interactiveAnimator.completionSpeed = 0.999;

bug reported here: http://openradar.appspot.com/14675246

Hyperextension answered 23/3, 2017 at 9:42 Comment(3)
The problem manifests itself most easily in the simulator, but with sufficiently complicated views, you can see it on devices, too.Silk
This must be the "Accepted" answer! Solved the problem for iOS 10.3Lathy
I just want to add that the problem arises in simulator and on device, at the end and at the beginning of the animation.Sharper
M
6

I've seen something similar. I have two possible workarounds. One is to use delayed performance in the animation completion handler:

} completion:^(BOOL finished) {
        double delayInSeconds = 0.1;
        dispatch_time_t popTime = 
             dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC));
        dispatch_after(popTime, dispatch_get_main_queue(), ^(void){
            BOOL cancelled = [transitionContext transitionWasCancelled];
            [transitionContext completeTransition:!cancelled];
        });
       self.interacting = NO;
}];

The other possibility is: don't use percent-drive animation! I've never had a problem like this when driving the interactive custom animation myself manually.

Monkeypot answered 13/4, 2014 at 21:54 Comment(1)
Elegant in its simplicity. The delay fixed it on both simulator and device, though I'll probably retire the UIPercentDrivenInteractiveTransition altogether. Good to know that there is such an easy work-around, though.Silk
A
0

The reason for this error in my case was setting the frame of the view being animated multiple times. I'm only setting the view frame ONCE and it fixed my issues.

So in this case, the frame of "toViewController.view" was set TWICE, thus making the animation have an unwanted behavior

Augustineaugustinian answered 14/5, 2015 at 19:57 Comment(1)
what's wrong with setting it twice? I mean you can for example set the toView frame to the left of the screen and then animate it to the position of the screen. What exactly am I missing?Hyperextension

© 2022 - 2024 — McMap. All rights reserved.