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.
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:
- Only the from view controller’s view is visible.
- 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. - 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.
- 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
- 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.
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.
- 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 [↩]
- 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. [↩]
- I like to think of the identity transform as a view’s native state. [↩]