Sep 20 2013

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 BNRCustomCollectionVC.

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.

    - (void)animateTransition:(id)transitionContext

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]);

And saw 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 initialFrameForViewController:

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.

17 Comments

  1. Nicolas Manzini

    Nice tutorial thank you, the WWDC 2013 wasn’t very clear on how those new transitions can be set. Do you know how to implement this with storyboard and custom segue?

    • Storyboards are just another way to layout view controllers and their views. You can do whatever you normally would within their class implementation – including what I demonstrate above. Using a custom segue is different than using custom transitions. To do what I’m talking about in this post, you’d use a push or modal transition, and in the `prepareForSegue:` method you would make sure your delegates are set up properly. That’s either the Navigation Delegate (for push) or the destination view controller’s transition delegate (for modal).

      • Andrew

        Thanks for the above example, I’ve got it working as a modal, but how would you change the code to make it work with a push on a navigation controller?

        • Instead of setting the destination view controller’s “transitionDelegate”, you need to set the navigation controller’s delegate. You need to implement a different method to supply the animator – see the nav controller delegate protocol. One caution: nav controller holds its delegate weakly, so you’ll need to make sure something else is retaining your delegate object. This surprised me a little. I haven’t thought through why that is the case.

          • Programming Thomas

            Could you clarify which function you need to implement for navigation controller transitions? I’ve tried setting the navigation controller’s transitioning delegate (which is retained by the app delegate) however it is still using the system transition.

          • https://developer.apple.com/library/ios/documentation/uikit/reference/UINavigationControllerDelegate_Protocol/Reference/Reference.html#//apple_ref/occ/intfm/UINavigationControllerDelegate/navigationController:animationControllerForOperation:fromViewController:toViewController:

            The first method under “Supporting Custom Transition Animations” in the UINavigationControllerDelegate is the one you’re looking for.

    • Jesse Wolff

      For storyboard segue examples see my unofficial sample code for WWDC session 218:
      https://github.com/soleares/SOLPresentingFun

  2. Craig

    I noticed that it doesn’t handle rotations very well. What would you need to change to to get it to handle being presenting in landscape?

    • That’s a great question, Craig. I’m not sure. I didn’t look at that for this demo. I do remember seeing discussion on the Apple dev forums, of the provided frame/container not being correct in landscape. Not sure if that applies here, but that’s where I would start looking.

    • Here’s the thread I was thinking of:
      https://devforums.apple.com/message/855819#855819 (Re: Transition delegate coordinate system only reports portrait orientation)

      Near the end of the second and third page, Rich offers some good clarifications. We may need to work with the container’s bounds, and it matters whether we’re doing a modal presentation or not.

  3. Clément

    I’m trying to design my view controller in storyboard and present it.

    So I instantiate my view controller from storyboard :

    UIStoryboard *sb = [UIStoryboard storyboardWithName:STORYBOARD_NAME bundle:[NSBundle mainBundle]];
    MyViewController *viewController = (MyAccountViewController *)[sb instantiateViewControllerWithIdentifier:@"MyViewController"];

    Then, I present it :

    viewController.transitioningDelegate = self;
    viewController.modalPresentationStyle = UIModalPresentationCustom;
    [_parentViewController presentViewController:viewController animated:YES completion:nil];

    But when I’m trying to make a custom transition, I can’t get the frame of my view in storyboard. It should be my end frame :

    - (void)animateTransition:(id)transitionContext
    {
    UIView *container = transitionContext.containerView;

    UIViewController *fromVC = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
    UIViewController *toVC = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
    UIView *fromView = fromVC.view;
    UIView *toView = toVC.view;

    CGRect screenRect = [UIScreen mainScreen].bounds;
    CGRect beginFrame = CGRectMake(screenRect.size.width/2, screenRect.size.height/2, 0, 0);

    CGRect endFrame = [transitionContext initialFrameForViewController:toVC];
    NSLog(@"%f, %f, %f, %f", endFrame.origin.x, endFrame.origin.y, endFrame.size.width, endFrame.size.height);

    UIView *move = nil;
    if (toVC.isBeingPresented) {
    toView.frame = endFrame;
    move = [toView snapshotViewAfterScreenUpdates:YES];
    move.frame = beginFrame;
    } else {
    move = [fromView snapshotViewAfterScreenUpdates:YES];
    move.frame = fromView.frame;
    [fromView removeFromSuperview];
    }
    [container addSubview:move];

    [UIView animateWithDuration:TRANSITION_DURATION delay:0
    usingSpringWithDamping:500 initialSpringVelocity:15
    options:0 animations:^{
    move.frame = toVC.isBeingPresented ? endFrame : beginFrame;}
    completion:^(BOOL finished) {
    if (toVC.isBeingPresented) {
    [move removeFromSuperview];
    toView.frame = endFrame;
    [container addSubview:toView];
    }

    [transitionContext completeTransition: YES];
    }];

    }

    My log is : 0.000000, 0.000000, 0.000000, 0.000000

    I would like to display my view controller with the frame in storyboard but I can’t figure out how I’m supposed to do it.

    • It makes sense that your toViewController doesn’t yet have a frame. Since you’re taking control of the animation, the system can’t assume where you want to put the new view – or even what size you want the view to be! So, per the documentation, “For the view controller being presented, the value returned by this method is typically CGRectZero because the view is not yet on screen.” (https://developer.apple.com/library/ios/documentation/uikit/reference/UIViewControllerContextTransitioning_protocol/Reference/Reference.html#//apple_ref/occ/intfm/UIViewControllerContextTransitioning/initialFrameForViewController :)

      You have to be careful with both initial and final frame calls, as they may be zero or nonsense values, depending on what the system knows. So in this case, you may want to get the bounds from your containerView instead, for example.

      • Clément

        Thanks for your reply.

        I understand that system does not know the view frame before he displays it. But in storyboard I set an initial frame. So the system should know it even if it is not displayed yet, is it ?

        I’ve tried to set the frame programmatically before present it :

        MyViewController *viewController = (MyViewController *)[sb instantiateViewControllerWithIdentifier:@"MyViewController"];
        [viewController.view setFrame:CGRectMake(8, 28, 304, 512)];
        [_parentViewController presentViewController:viewController animated:YES completion:nil];

        And get it like this :
        CGRect endFrame = toView.frame;
        NSLog(@"%f, %f, %f, %f", endFrame.origin.x, endFrame.origin.y, endFrame.size.width, endFrame.size.height);

        I get this log :
        0.000000, 0.000000, 304.000000, 512.000000

        Here, system knows the width and the height before display the view but not the origin…
        I don’t understand when it knows and when it does not.

        PS : does not work in my posts…

        • I don’t have time at the moment to check your code. But I’m not sure why you’re trying to use ‘initialFrame’ for your final position. InitialFrame is at start of animation, not end. So it really doesn’t know where you want it to start. Are you trying to spring the view from offscreen to onscreen?

          • Clément

            I actually use the same transitioningDelegate for all my presented views but displaying them with different frames.
            So, I want to display my view from offscreen to onscreen using the frame defined at initialization.

          • Clément

            I’ve tried different ways to get the the frame defined at initialization.
            CGRect endFrame = [transitionContext initialFrameForViewController:toVC];
            CGRect endFrame = [transitionContext finalFrameForViewController:toVC];
            CGRect endFrame = toView.frame;

Leave a Comment

Join the discussion. Do not worry, your email address will not be published.

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>