Golden Opportunity: Custom Transitions in iOS 7
iOS 7 feels fresh and new, thanks in large part to its zooming, swooping, sliding interface. To me, it feels alive and fresh after several years of everything sliding in from the right. You want to add this yummy goodness to your app, right? Let’s get to it.
There isn’t a set of new built-in transitions here. Instead, Apple has exposed the pieces they use to make transitions happen and allowed us to hook in. We’ll start with a modal presentation from a collection view cell. To follow along in Xcode, check out the demo project Collections from our iOS 7 demos. Notice the collection view controller inside the tab bar controller, at the top of the storyboard. There’s also a
modalVC visible, but instead of using a segue, we’re going to add a custom presentation. Take a look in
Delegate the work
The first thing we need is an animation delegate. We set the delegate on the VC that is going to be presented:
toVC.transitioningDelegate = self; toVC.modalPresentationStyle = UIModalPresentationCustom;
We present the view controller just as before:
[self presentViewController:toVC animated:YES completion:nil];
To keep it simple, we’ll have the presenting VC conform to
UIViewControllerTransitioningDelegate. Now, when we present the VC, it asks its transition delegate for an animator by sending this message:
- (id)animationControllerForPresentedController:(UIViewController *)presented presentingController:(UIViewController *)presenting sourceController:(UIViewController *)source
Transitioning: Where the real work happens
Previously, doing our own navigation controller transition was an exercise in frustration. Now, the OS takes care of the wiring at the beginning and end to ensure consistent state, but turns the keys over to us for animation. We’re given a container view and a couple of frames, and are allowed to build a transition of arbitrary complexity. At the end, we need to leave the views in a consistent state. Specifically, we’re responsible for adding, moving, and removing views from the container view. The
UIViewControllerAnimatedTransitioning protocol is where we do the work of actually animating the transition.
Notice that the system is handing us a
transitionContext. You can see from the method declaration that the API doesn’t make any promises about the object itself, only that it conforms to a protocol. I was curious, so I did a bit of caveman debugging:
NSLog(@"context class is %@", [transitionContext class]);
context class is _UIViewControllerOneToOneTransitionContext. It might be fun to poke around and see if there are other transition context objects too. Just remember that these are private and not guaranteed by the API.
Let’s get back to business and look at the transitioning protocol:
... Accessing the Transition Objects – containerView required method – viewControllerForKey: required method …
Right at the top, we see a couple of curious methods. The context is giving us a place to add and remove views. That’s the container view. We can also get the the View Controllers from both ends of our transition:
UIView *container = transitionContext.containerView; UIViewController *fromVC = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey]; UIViewController *toVC = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
Let’s first set up the end of the transition correctly, without any animation. This bit is tricky (by which I mean it tripped me up at first), so we’re going back to first programmer principles and verify our assumptions at each step of the way. We know from Apple’s WWDC sessions (218 and 226) that we are responsible for inserting the new view into the hierarchy, but we don’t exactly know how to do so. So let’s verify some of the new API (Xcode 5 makes this much easier via breakpoints and the variable inspector, or you can bust out NSLog if you want):
CGRect initialFromFrame = [transitionContext initialFrameForViewController:fromVC]; CGRect finalToFrame = [transitionContext finalFrameForViewController:toVC]; (And so on)
Ah, as the documents say for
“The rectangle returned by this method represents the size of the corresponding view at the beginning of the transition. For the view controller being presented, the value returned by this method is typically
CGRectZero because the view is not yet on screen.”
And indeed, we do see several
CGRectZero‘s. We also see some unexpectedly large frames. Because we’re doing a custom modal presentation, the context didn’t know what the final frame should be! So it bailed on us, and we have to decide ourselves. Fair enough, I guess. In this case we’ll take the easy way out. The initial frame for our
FromViewController looks good, so we use that.
CGRect endFrame = [transitionContext initialFrameForViewController:fromVC];
We’re just about there. Let’s set up our final view first, without any animation:
toView.frame = endFrame;
Last, we must tell the context when we’re done, so it can jiggle some more wires in the background to make sure we have a sane view controller and view hierarchy:
[transitionContext completeTransition: YES];
Try running it. It should work! But without any lovely animations. Fortunately,
UIView offers some convenient animation methods for us. Try the new Collections demo project for a zooming, springy animation, then let your imagination loose! Grab a snapshot view, change some properties and animate however you’d like. I am looking forward to seeing what you come up with.