Why the pause between animateWithDuration animation and completion blocks?
N

3

7

Following Apple's recommendations, I'm chaining UIView animations by putting subsequent calls to -animationWithDuration:animation: in the completion: block of another call to aanimateWithDuration:animation:completion:, like so:

[UIView animateWithDuration:scaleDuration delay:0 options:UIViewAnimationOptionCurveEaseIn animations:^{
    // Scale the controllers' views down.
    self.view.transform = CGAffineTransformScale(self.view.transform, 0.8, 0.8);
} completion:^(BOOL finished) {
    // Transition to the new view and push on the new view controller.
    [UIView transitionWithView:self.view duration:1 options:UIViewAnimationOptionCurveLinear | UIViewAnimationOptionTransitionFlipFromLeft animations:^{
        [self pushViewController:viewController animated:NO];
    } completion:^(BOOL finished) {
        [UIView animateWithDuration:scaleDuration delay:0 options:UIViewAnimationOptionCurveLinear animations:
^{
            // Scale back to the original size.
            self.view.transform = CGAffineTransformScale(self.view.transform, 1.25, 1.25);
        } completion:nil];
    }];
}];

The animations all execute the right order, but there is a tiny delay between them, especially before the -transitionWithView:duration:options:animations:completion: call. How do I smooth out the transitions between animation steps?

Neddie answered 6/5, 2012 at 5:21 Comment(13)
Is self in this context a navigation controller as your sending pushViewController: to it? and what is the intended animation, "flip" between views instead of sliding left/right as with a normal navigation controller?Radian
Ok. Do the view controllers that get pushed implement loadView, viewDidLoad, viewWillAppear or viewWillLayoutSubviews? if so check if they get called during the animation and make sure they don't do anything that takes time.Radian
Sorry for the delayed reply, @MattiasWadman, I was away at a conference all last week. Yes, it loads an AQGridView in viewWillAppear, which in turn loads an NSFetchedResultsController and a bunch of views with images loaded from disk. That obviously might incur some overhead. I wonder if I can get it to do that before the animation starts…Neddie
Ok. Yes the images sounds like a good point to start. First try to just skip loading them an check if you see and difference. Maybe you can preload the images somehow? a bit ugly solution is to let the view controller load it in its init an then pass it as an argument when creating the view using it etc.Radian
Thanks for checking back, @MattiasWadman. If I cut down on the work done in gridView:cellForItemAtIndex:, it definitely reduces the time, especially if I remove the image loading. There is still a slight pause, but greatly reduced. I just need to figure out how to get that stuff to load before the animation starts.Neddie
And the cells are loaded by layoutSubviews in AQGridView. I tried calling it before starting the animation, but that had no effect. Any ideas how to get it to load all the cells before starting the animation?Neddie
You could try to preloading the view managed by the view controller, if you access the view property manually it will cause the view controller to call loadView (and viewDidLoad etc). Try to do a dummy [self view] call after the init somewhere, not sure if it's good or bad to do inside the view controller init.Radian
A more clean solution i think is to preload a cache with images etc that take time to load or to do heavy loading in a background thread and send and update to the main ui thread when done.Radian
Well @MattiasWadman, that sounds non-trivial, and also means that the view might display during the animation not fully loaded: that is, without the images displaying during the animation, but appearing afterward. Not to keen on that. I have had a [viewController view] call in the code per @danyowdee's answer, but layoutSubviews does not get called. :-(Neddie
Ah ok. Yeah calling view is probably a bit of hack. Then I guess you need to load the image (or images?) before init (at app start?) and keep a reference is some global object that you can call. Is here a lot of images?Radian
Running it through instruments, it looks like about half the time is taken up fetching each object from the NSFetchedResultsController, and the other half loading the disk. There are 12 images displayed initially, so not a huge number. I added code to viewDidLoad that simply fetches each of the first 12 items and loads their images, but it doesn't seem to make much difference. Yes, it runs, but perhaps the NSManagedObject returned by objectAtIndexPath:indexPath: aren't cached? :-(Neddie
Sorry haven't used core data myself that much. But can't you do all loading in init instead and keep references that you then use when loading the view.Radian
Probably, but given that I have a deadline to meet, and I can go with a different animation, I am giving up on this for now. Thanks for the help!Neddie
S
2

Aside:
Is there any particular reason why you are abusing a navigation controller in this way? Couldn’t you just use presentViewController:animated:completion: setting its transition style to UIModalTransitionStyleFlipHorizontal?

Back to your question:

I’m pretty much sure that the stutter comes from the simple fact that pushViewController:animated: implicitly has to load the view of the view controller that is to be pushed and that this takes some time.

So if you cannot use the presentViewController:animated:completion: (or presentModalViewController:animated: if you have to support iOS 4) approach, I’d encourage you to try this:

// ensure the view is already loaded when you invoke `pushViewController:animated:`
[viewController view];

// there is no strict need to calculate those in situ, so we do it up front
CGAffineTransform originalTransform = self.view.transform;
CGAffineTransform downscalingTransform = CGAffineTransformScale(originalTransform, 0.8, 0.8);

// I found it hard to read the block-in-a-block-in-a... while editing here, so I've taken them apart:
void (^scaleDownAnimation)() = ^{
    self.view.transform = downscalingTransform;
};

void (^restoreScaleAnimation)() = ^{
    self.view.transform = originalTransform;
};

void (^pushControllerAnimation)() = ^{
    [self pushViewController:viewController animated:NO];
};

void (^pushAnimationCompletion)(BOOL) = ^(BOOL unused) {
    [UIView animateWithDuration:scaleDuration
                          delay:0
                        options:UIViewAnimationOptionCurveLinear
                     animations:restoreScaleAnimation
                     completion:nil];
};

void (^downscaleCompletion)(BOOL) = ^(BOOL unused){
    UIViewAnimationOptions linearFlipFromLeft = UIViewAnimationOptionCurveLinear | UIViewAnimationOptionTransitionFlipFromLeft;
    [UIView transitionWithView:self.view
                      duration:1
                       options:linearFlipFromLeft
                    animations:pushControllerAnimation
                    completion:pushAnimationCompletion];
};

[UIView animateWithDuration:scaleDuration
                      delay:0
                    options:UIViewAnimationOptionCurveEaseIn
                 animations:scaleDown
                 completion:downscaleCompletion];

Note that the beef is within the first six lines, all the rest is just for completeness.

Shannanshannen answered 19/5, 2012 at 8:47 Comment(11)
Thanks @danyowdee. Is the principal point to load the view before performing the animation, so that we eliminate that overhead during the animation? Or do you also think it's important to try to remove all the calculations from the animation blocks (e.g., the downscalingTransform)? I have just loading the view, and am not seeing much difference. Will keep poking at it…Neddie
I commented out the code in viewWillAppear:animated, and the pause was substantially reduced, though not eliminated. So it seems clear that it's due to stuff happening during the transition. I just need to figure out how to make it all happen before the animation starts.Neddie
Hrm. If I call viewWillAppear: before pushing, or move the loading of the AQGridView to viewDidLoad, I still get the pause. But if I don't load the AQGridView at all, there is only a very slight pause. I wonder what gets deferred by the loading the AQGridView?Neddie
Oh, and to answer your aside, my designer partner came up with a pretty nice transition animation that involves moving the toolbar off the screen, scaling the visible view down to 80%, then doing the flip animation, then scaling back up and returning the toolbar (with different items). It's pretty nice, and fits well with the two aspects of our app, so I'm trying to get it as smooth as possible, naturally.Neddie
So the issue is that simply calling [viewController view] does not cause the view to load: layoutSubviews isn't called, and that's where all the loading takes place. This does seem to be the root of the problem; I just need to figure out how to get it to do all that work before starting the animation. Any other suggestions?Neddie
Oh that’s a bit of a problem! Why do you load/create subviews in layoutSubviews? That method should just… lay them out. What’s your setup? Are you using NIBs or creating your hierarchy programmatically? In the latter case, override loadView to build up your subviews. In the former, you can customize do additional setup in viewDidLoad.Shannanshannen
I don't. AQGridView does.Neddie
Oh. I see. Random hack: include [self.view layoutSubviews] in that view-controller’s viewDidLoad? That’s far from pretty, but at a glance it looks like that would pre-populate the tile-queue…Shannanshannen
More to the point on the UIModalTransitionStyleFlipHorizontal suggestion: that's a modal transition, not a UINavigationController transition. One has to do more work to add custom transitions, as described in this question.Neddie
I tried calling layoutSubviews before starting the animation, but it did not populate the grid cells. It looks as though that might be a no-op when the view does not have a parent. I could maybe add it to a view, call layoutSubviews, then remove it from the view. Nasty, but might work…Neddie
Ultimately this does seem to be the issue: need to load everything before any of the animation starts. See also the exchange with @MattiasWadman in the comments on the question itself. So I'm accepting this answer, even though I have not been able to get it to work and have abandoned this problem for now. I will come back to it later, though, and look forward to reading through all this stuff again. :-)Neddie
P
0

I've chained animations as you describe with no delay.

You are using transitionWithView:duration:options:animations:completion: with the animation block being a call to pushViewController:animated:. That doesn't make any sense to me.

transitionWithView:duration:options:animations:completion transitions from one subview to the next.

pushViewController:animated is a navigation controller method that pushes a view controller on the navigation controller stack. Using those 2 together makes no sense to me.

If you want your scale animation to end with a push, then just call the push animation directly from your completion block.

Pythia answered 16/5, 2012 at 22:6 Comment(3)
I do not want it to end with a push. I want it to flip the views, not push them. So the transitionWithView:duration:options:animations:completion: call does the flipping, with pushViewController:animated: doing the actual work of switching the views. I set animated:NO so that it does not animate. It looks pretty good, except for the very slight pause.Neddie
BUt pushViewController:animated: shouldn't be used to SWITCH VIEWS. Only view controllers. You can switch the views with animations inside your controller. That's the other thing.Overton
pushViewController:animated is the only way to push a view controller onto a UINavigationController, AFAIK. Which is what I am doing. My apologies for saying "view" when I meant "view controller."Neddie
F
0

In my experience, the "use the completion block to chain animations" is simply flawed (makes small pauses between sub-animations). I made sure there's no loadView/viewDidLoad/viewWillAppear/viewWillLayoutSubviews abuse going on.

The exact same code stutters with "completion-block_chainig" but works just fine if I rework it to use keyframes.

So, instead of (for example)...

        UIView.animate(withDuration: 1, animations: {[weak self] in
            ...
            self?.view.layoutIfNeeded()
        }, completion: { [weak self] _ in
            UIView.animate(withDuration: 1, animations: {
                ...
                self?.view.layoutIfNeeded()
            }, completion: { [weak self] _ in
                UIView.animate(withDuration: 1, animations: {
                    ...
                    self?.view.layoutIfNeeded()
                }, completion: { [weak self] _ in
                    ...
                })
            })
        })

... I instead prefer...

        UIView.animateKeyframes(withDuration: 3, delay: 0, options: .calculationModeLinear, animations: {[weak self] in

            UIView.addKeyframe(withRelativeStartTime: 0, relativeDuration: 0.33, animations: {[weak self] in
                ...
                self?.view.layoutIfNeeded()
            })

            UIView.addKeyframe(withRelativeStartTime: 0.33, relativeDuration: 0.33, animations: {[weak self] in
                ...
                self?.view.layoutIfNeeded()
            })

            UIView.addKeyframe(withRelativeStartTime: 0.66, relativeDuration: 0.33, animations: {[weak self] in
                ...
                self?.view.layoutIfNeeded()
            })

        }, completion: { [weak self] _ in
            ...
        })
Francoisefrancolin answered 17/5, 2017 at 15:30 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.