sohail.io

  • About

iOS7 View Controller Transitioning without Frames

October 20, 2013

View Controller Transitioning was introduced with iOS7. Previously, the out-of-the-box tools had developers relying on UIKit container controllers such as UINavigationController and UITabBarController to switch between view controllers.

Of course, there was always modal presentation of view controllers available and some basic transitions styles, such as sliding up from the bottom or flipping along the y-axis. In iOS5, View Controller Containers were made available. This finally provided some real structure to custom view transitions for view controllers.

If your app doesn’t require explicit container controllers however, I would argue that the new iOS7 API for View Controller Transitioning is simpler. In fact, if you want interactive transitions, then the choice becomes even more clear in favor of the View Controller Transitioning API. 

View Controller Transitioning is very much related to Container View Controllers, though we are only exposed to the container view concept in the Transitioning API. We don’t have to add and remove child view controllers as we would with full-on view controller containment approaches. In many ways, this makes a custom transition between two view controllers simpler to code.

This article discusses the use of the UIViewControllerContextTransitioning protocol’s methods for retrieving final frames for view controllers participating in the transition and how this API can be problematic. This article’s suggested approach is to rely on transforms instead. The majority of this article walks through how an animated translation transform can be used to implement a modal transition. This is still useful because the stock modal transition does not support keeping the ‘from’ view controller on screen, which is crucial if you want to create a translucent effect (useful should you decide on replacing the stock UIActionSheet or UIAlertView components).

For a proper introduction to this topic, see the WWDC 2013 video for session 218: Custom Transitions Using View Controllers.

A Typical Example

Recently, I was working through some examples on UIViewController transitioning. The project didn’t use Auto Layout. What it did make use of however, were methods from the UIViewControllerContextTransitioning protocol for obtaining the final frame of the to view controller: 

- (CGRect)finalFrameForViewController:(UIViewController *)vc

Upon running the completed project, I stepped through the method:

- (void)animateTransition:(<UIViewControllerContextTransitioning>)transitionContext

I did indeed confirm that the frame for the to view-controller was as I would have expected. This CGRect had an origin of {0,0} and a size that matched the screen.1

Here’s a typical animateTransition:method that demonstrates this usage:2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
 
- (void)animateTransition:(id)transitionContext {
    // 1. Obtain state from the context
    UIViewController *toViewController = [transitionContext
viewControllerForKey:UITransitionContextToViewControllerKey];
    CGRect finalFrame = [transitionContext finalFrameForViewController:toViewController]; // Notice this!
 
    // 2. Obtain the container view
    UIView *containerView = [transitionContext containerView];
 
    // 3. Set the initial state
    CGRect screenBounds = [[UIScreen mainScreen] bounds];
    toViewController.view.frame = CGRectOffset(finalFrame, 0, screenBounds.size.height);
 
    // 4. Add the view
    [containerView addSubview:toViewController.view];
 
    // 5. Animate
    NSTimeInterval duration = [self transitionDuration:transitionContext];
 
    [UIView animateWithDuration:duration animations:^{
        toViewController.view.frame = finalFrame;
     } completion:^(BOOL finished) {
        // 6. Inform the context of completion
        [transitionContext completeTransition:YES];
    }];
}
 

CGRectZero and the Final Frame

Beyond the tutorial, in building my own apps, I noticed that I’d often get a CGRectZero for the finalFrame variable at the end of Step 1 (see code listing above). While the tutorial project did not use Auto Layout, my apps did. Both used Storyboards. Now Auto Layout may not have been responsible for this difference, but given that I was using Auto Layout, it occurred to me that I really didn’t want to be setting view controller frames manually anyways.

Technically, the containing view for this transition is going to treat the from and the to view controller views as black boxes and not care whether their internals are Auto Layout based — or would it? Recall that whether you layout a view controller in Interface Builder in 3.5″ form or 4″ form (assuming iPhone form factors), the views generally always render full screen correctly. Why? Because there’s an auto-resizing mask to have them pinned to their containing view, such as their containing UIWindow (if there are no intermediary views).

API Guidance

Perhaps there’s a difference in runtime handling of child views that are Auto Layout based versus those that are not. This difference I’ve come across might be a non-causal correlation. Regardless, Apple does caution in the API docs, that these frame calls can often return CGRectZero (effectively giving you no information):

Return Value The frame rectangle for the view or CGRectZero if the frame rectangle is not known or the view is not visible.
Discussion The rectangle returned by this method represents the size of the corresponding view at the end of the transition. For the starting view controller, the value returned by this method might be CGRectZero if the corresponding view was completely covered during the transition, but it might also be a valid frame rectangle.

Exactly what I experienced. As mentioned, given that I was using Auto Layout, manually setting frames seemed out of place. Furthermore, I would expect that at the start of the transition, both the from and the to view controller are at origin {0,0} and at their regular size (assuming no transforms had been applied). This is what I experienced if I just added the to view controller’s view to the container view and called the completeTransition: method in animateTransition:, without doing anything else:

1
2
3
4
5
6
7
8
9
 
- (void)animateTransition:(id)transitionContext {
    UIViewController *toViewController = [transitionContext
viewControllerForKey:UITransitionContextToViewControllerKey];
    UIView *containerView = [transitionContext containerView];
    [containerView addSubview:toViewController.view];
    [transitionContext completeTransition:YES];
}
 

It seemed to me that dealing with transforms was a better way to go for the general case.

Modal Presentation with Transparency

My original goal was to build a modal transition where the from view controller could still be seen through the to view controller that slid up from the bottom of the screen.

This is very much the stock modal presentation transition in iOS with one key difference: the from view controller’s view is not hoisted off screen when the to view controller has completed its transition.

The ability to control what happens to the from view controller’s view in such transitions is what makes the new iOS7 View Controller Transitioning API so useful.

The diagram below illustrates our hypothetical from and to view controllers. The to view controller has a semi-transparent background, so that when placed on top of another view controller, we’ll achieve a translucent look.

To and From View Controllers, Starting Out

Once we enter the animateTransition: method (assuming we’re presenting the to view controller), we progress through the following stages as illustrated in the next diagram:

  1. Only the from view controller’s view is visible.
  2. We add the to view controller’s view to the container view, but this is never actually seen by the user. Conceptually, this is stage 2 in the diagram below. If we did nothing else but call completeTransition:, then this is what we would have seen.
  3. We apply a transform on the to view controller’s view. Specifically, one that translates the view in the y-axis by exactly the amount that is our screen height.
  4. The animation begins. The progression of the animation we see is really the animation of the view’s transform property, animating from the starting translation transform we gave it, all the way back to the identity transform.3
  5. The animation completes. We’ve called completeTransition: on the transition context. The from view controller is still on screen and because the to view controller is semi-transparent, we have a translucent effect.

Progression through steps in animateTransition: call

Here’s the code I wrote that uses translation transforms to achieve initial and final placement. The animation used at the end takes advantage of the new spring (Dynamics) based UIKit animation API.

Header

1
2
3
4
5
6
7
 
#import <Foundation/Foundation.h>
 
@interface IDSModalAnimatedTransitioningController : NSObject
@property (nonatomic, assign) BOOL reverse;
@end
 

Implementation

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
 
#import "IDSModalAnimatedTransitioningController.h"
 
@implementation IDSModalAnimatedTransitioningController
 
- (NSTimeInterval)transitionDuration:(id)transitionContext {
    return 0.5;
}
 
- (void)animateTransition:(id)transitionContext {
    // Obtain state from the Context:
    UIViewController *toViewController = [transitionContext
viewControllerForKey:UITransitionContextToViewControllerKey];
    UIViewController *fromViewController = [transitionContext
viewControllerForKey:UITransitionContextFromViewControllerKey];
 
    // Obtain the container view:
    UIView *containerView = [transitionContext containerView];
 
    // Set the initial state:
    CGRect screenBounds = [[UIScreen mainScreen] bounds];
 
    CGAffineTransform completionTranslationTransform;
    UIViewController *animatingViewController;
 
    // Are we being asked to reverse the animation (i.e. dismissal)?
    if (self.reverse) {
        // YES: We're going to dismiss:
        animatingViewController = fromViewController;
        completionTranslationTransform = CGAffineTransformMakeTranslation(0, screenBounds.size.height);
    }
    else {
        // NO: We're going to present:
        animatingViewController = toViewController;
        CGAffineTransform startingTranslationTransform =
CGAffineTransformMakeTranslation(0, screenBounds.size.height);
        completionTranslationTransform = CGAffineTransformIdentity;
 
        // Set the toViewController to its initial position, since it wouldn't be on screen as yet:
        toViewController.view.transform = startingTranslationTransform;
 
        // Add the view of the incoming view controller:
        [containerView addSubview:toViewController.view];
    }
 
    // Animate
    NSTimeInterval duration = [self transitionDuration:transitionContext];
 
     [UIView animateWithDuration:duration
                           delay:0.0
          usingSpringWithDamping:0.75
           initialSpringVelocity:0.35
      options:UIViewAnimationOptionCurveLinear
                      animations:^{
         animatingViewController.view.transform = completionTranslationTransform;
     }
     completion:^(BOOL finished) {
         if (self.reverse) { [fromViewController.view removeFromSuperview]; }
         // Inform the context of completion:
         [transitionContext completeTransition:YES];
     }];
}
 
@end
 

Notice how in the forward case (reverse == FALSE) where we are presenting the to view controller, we create a startingTranslationTransform on it, where the view is translated downwards by the height of the current device’s screen. This ensures that it is off-screen to start.

When the animation completes, we want the to view controller to be positioned where it started at the beginning of this method — at origin {0,0}. For that, all we need to do is replace the transform applied with the identity transform, CGAffineTransformIdentity.

Note that to achieve the same goal I set out to, you’ll need to have a to view controller in your app with a semi-transparent background.

Wrap Up

The sample IDSModalAnimatedTransitioningController class didn’t ask for or set any view controller frames, and the translation transform technique will work whether you’re using Auto Layout or manual layout; and whether finalFrameForViewController: returns a meaningful CGRect or just returns CGRectZero (because you won’t be basing your setup or animations off of this information).

You can find the source for the modal animated transitioning controller on github.

  1. You would expect that if the design in Interface Builder did not artificially constrain the view controller’s view dimensions and/or the default auto-resizing masks were left in place [↩]
  2. This example is from a post on Stackoverflow.com referencing code in a forum on RayWenderlich.com based on an excellent tutorial in the book iOS7 by Tutorials. The code excerpt I’ve posted is slightly tidied up from the source. [↩]
  3. I like to think of the identity transform as a view’s native state. [↩]

Filed Under: iOS Tagged With: auto-layout, iOS7, transitioning, view controllers

search

Categories

  • General
  • iOS

Recent Posts

  • AppleScript Export to CSV via iWork Numbers
  • Verifying the Status of an Auto-Renewable Subscription
  • Swift Memory Management Exercise
  • Learning Swift: My Approach
  • UIAutomation Command Line Test Runner for Xcode 6

This Blog

My name is Sohail. I'm a developer-consultant and entrepreneur primarily focused on building high quality iOS apps.

I write about software development. Mostly iOS related and sometimes, Ruby on Rails.

See the About page for more bio.

Sohail Ahmed - About page for blog author bio

Category Specific RSS

  • General (2)
  • iOS (6)

Archives

  • October 2015 (2)
  • June 2015 (2)
  • September 2014 (1)
  • October 2013 (3)

Copyright © 2021 Sohail A. · Log in

All opinions expressed are strictly those of the author, and do not necessarily reflect those of guests, partners, sponsors, customers or affiliates.