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?

theory
  • 9,178
  • 10
  • 59
  • 129
  • 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? – Mattias Wadman May 16 '12 at 20:13
  • 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. – Mattias Wadman May 17 '12 at 16:01
  • 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… – theory May 22 '12 at 03:54
  • 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. – Mattias Wadman May 22 '12 at 08:25
  • 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*. – theory May 24 '12 at 03:39
  • 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? – theory May 24 '12 at 03:53
  • 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. – Mattias Wadman May 24 '12 at 08:29
  • 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. – Mattias Wadman May 24 '12 at 08:34
  • 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. :-( – theory May 24 '12 at 21:52
  • 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? – Mattias Wadman May 24 '12 at 21:56
  • 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? :-( – theory May 31 '12 at 07:22
  • 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. – Mattias Wadman May 31 '12 at 08:02
  • 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! – theory Jun 02 '12 at 03:13

3 Answers3

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.

danyowdee
  • 4,658
  • 2
  • 20
  • 35
  • 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… – theory May 22 '12 at 04:05
  • 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. – theory May 22 '12 at 04:14
  • 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? – theory May 22 '12 at 05:58
  • 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. – theory May 22 '12 at 06:00
  • 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? – theory May 24 '12 at 04:00
  • 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`. – danyowdee May 24 '12 at 08:29
  • I don't. [AQGridView does](https://github.com/AlanQuatermain/AQGridView/blob/master/Classes/AQGridView.m#L651). – theory May 24 '12 at 21:48
  • 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… – danyowdee May 24 '12 at 22:40
  • 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](http://stackoverflow.com/q/1406037/79202). – theory May 31 '12 at 05:12
  • 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… – theory May 31 '12 at 05:15
  • 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. :-) – theory Jun 02 '12 at 03:15
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.

Duncan C
  • 128,072
  • 22
  • 173
  • 272
  • 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. – theory May 17 '12 at 15:48
  • 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. – kender May 18 '12 at 12:51
  • `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." – theory May 22 '12 at 03:57
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
            ...
        })
Florin Odagiu
  • 456
  • 6
  • 16