Written by Brightec Team
Jan 27, 2014

iOS 7 Custom View Controller Transitions and Rotation, making it all work

Custom iOS7 Transition

Slowly making sense

iOS 7 introduced a whole bunch of new APIs for developers to sink their teeth into but one of the standout APIs is the ability to create custom UIViewController transitions. Unfortunately it's also one of the most confusing.

Once you get your head around-it though, it slowly starts to make sense and seem less daunting. There are a lot of good tutorials on Google so I won't be going over the basics here, this post assumes you already have some knowledge in using this new API.

One area that left me puzzled though was making these transitions work in any orientation.

Rotation and the new iOS 7 Transition API

Let's have a look at the issue we faced with rotation and the new iOS 7 Transition API.

We wanted to create a very simple custom UIViewController transition that would slide the presenting UIViewController from the bottom of the screen, similar to the new Control Centre in iOS 7.

Implementing this is actually fairly easy, you just implement the UIViewControllerTransitioningDelegate protocol and it's animationControllerForPresentedController:presentingController:sourceController: method and animationControllerForDismissedController:dismissed method and return another object from these methods that implements the UIViewControllerAnimatedTransitioning protocol.

In our case we decided to just have a single class that subclasses NSObject that implements both these protocols.

@interface BTSlideInteractor : NSObject 
@property (nonatomic, assign, getter = isPresenting) BOOL presenting;
@end

As your can see in our example header code above our BTSlideInteractor class implements both protocols, because our use-case is quite simple we felt it would be overkill to create two objects.

You'll notice that we've created a property called presenting with the custom getter name isPresenting.

This property will be used as part of the UIViewControllerAnimatedTransitioning methods to determine if we should be performing the presentation animation of the dismissal animation.

Obviously you can have two different transitions for the presentation and dismissal but again our use-case was to have a simple slide up and down animations and will use shared code.

# pragma mark -
# pragma mark UIViewControllerTransitioningDelegate

- (id )animationControllerForPresentedController:(UIViewController *)presented presentingController:(UIViewController *)presenting sourceController:(UIViewController *)source
{
    self.presenting = YES;
    return self;
}


- (id )animationControllerForDismissedController:(UIViewController *)dismissed
{
    self.presenting = NO;
    return self;
}

Above if a snippet from the implementation file. These are the two methods that you must implement as defined in the UIViewControllerTransitioningDelegate protocol.

As mentioned above you must return an object that implements the UIViewControllerAnimatedTransitioning protocol, in our-case we will assign it to self as our object implements both protocols.

We set the presenting property here so that our animation delegate performs the correct animation. To do the actual animation your must implement the transitionDuration: and animateTransition: methods of the UIViewControllerAnimatedTransitioning protocol. Let's take a look at our original implementation:

# pragma mark -
# pragma mark Helpers

const CGFloat PresentedViewHeightPortrait = 720.0f;
const CGFloat PresentedViewHeightLandscape = 440.0f;

- (CGRect)rectForDismissedState:(id)transitionContext
{
    UIView *containerView = [transitionContext containerView];
    
    CGRect frame = CGRectMake(0, containerView.bounds.size.height, containerView.bounds.size.width, 720.0f);
    return frame;
}


- (CGRect)rectForPresentedState:(id)transitionContext
{
    UIViewController *controller;
    if (self.presenting) {
        controller = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
    } else {
        controller = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
    }
    
    CGRect frame = CGRectOffset([self rectForDismissedState:transitionContext], 0, -CGRectGetHeight(controller.view.bounds));
    return frame;
}

# pragma mark -
# pragma mark UIViewControllerAnimatedTransitioning

- (void)animationEnded:(BOOL)transitionCompleted
{
    // reset state
    self.presenting = NO;
}


- (NSTimeInterval)transitionDuration:(id)transitionContext
{
    return 0.3f;
}


- (void)animateTransition:(id)transitionContext
{
    UIViewController *fromViewController = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
    UIViewController *toViewController = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
    UIView *containerView = [transitionContext containerView];
    
    if (self.presenting) {
        // set starting rect for animation
        toViewController.view.frame = [self rectForDismissedState:transitionContext];
        [containerView addSubview:toViewController.view];
        
        [UIView animateWithDuration:[self transitionDuration:transitionContext] animations:^{
            toViewController.view.frame = [self rectForPresentedState:transitionContext];
        } completion:^(BOOL finished) {
            [transitionContext completeTransition:YES];
        }];
    } else {
        
        [UIView animateWithDuration:[self transitionDuration:transitionContext] animations:^{
            fromViewController.view.frame = [self rectForDismissedState:transitionContext];
        } completion:^(BOOL finished) {
            [transitionContext completeTransition:YES];
            [fromViewController.view removeFromSuperview];
        }];
    }
}

There is where the real magic happens.

The key method is the animateTransition: method. This is where you perform your animation. You're given an transitionContext object which gives you access to the From View Controller and the To View Controller. You're also given a containerView which acts as a kind of proxy superview for your From and To view controllers.

You can see in our code above that our implementation is very simple. We use the UIView block based animation methods to animate the frame of the toViewController. When presenting we slide up the toViewController's view from the bottom and when dismissing we slide down until the view is now longer visible.

We've created a couple of helper methods to calculate the rects which you can see above. This is just to make the code more readable and as we also intend to implement the UIViewControllerInteractiveTransitioning protocol in the future (not covered in this blog post) which would rely on the same frame calculations so it seemed to make sense to break this logic out into separate methods. Though this is purely an implementation detail.

Watch this demo: https://vimeo.com/85142328

The Problem

As you can see in the above video that the custom transition works great when in portrait but not so great in landscape. The red view controller should appear from the bottom when in landscape but instead appears from the left edge of the screen. T

hat's not what we want. When investigating the problem we noticed that the containerView's bounds was always returning values as if the view was shown in portrait regardless of the orientation of the device. This had me quite puzzled as I would have expect to see the bounds width/height values flip around, i.e. in portrait the bounds is {0, 0}, {768, 1024}} and in landscape I would have expected the bounds to be {0, 0}, {1024, 768}} but instead the bounds where reported as {0, 0}, {768, 1024}}.

So it appears the containerView does not respect the orientation. To make matters more complicated it turned out that the containerView also has a transform applied. So we doubled checked that our code was using the bounds and not the frame to calculate the rects as frame can not be relied upon when a view has a transform, but no luck.

The Solution

After some Googling I could not find any reliable information on the issue and Apple's docs on the new API are lacking. I contacted Apple Developer Technical as a last resort and raised an support issue as it appeared that this is a bug.

Here is the response from Apple: "

For custom presentation transitions we setup an intermediate view between the window and the windows rootViewController's view. This view is the containerView that you perform your animation within. Due to an implementation detail of auto-rotation on iOS, when the interface rotates we apply an affine transform to the windows rootViewController's view and modify its bounds accordingly. Because the containerView inherits its dimensions from the window instead of the root view controller's view, it is always in the portrait orientation." "

If your presentation animation depends upon the orientation of the presenting view controller, you will need to detect the presenting view controller's orientation and modify your animation appropriately. The system will apply the correct transform to the incoming view controller but you're animator need to configure the frame of the incoming view controller."

The golden nugget of information here is that the containerView is not a subview of the rootViewController's view and that the containerView acts as if it is always in portrait. Seems like key information that really should by mentioned in Apple's documentation. To work around this we need to detect the orientation and swap around the values ourselves.

Here is the fixed code:

const CGFloat PresentedViewHeightPortrait = 720.0f;
const CGFloat PresentedViewHeightLandscape = 440.0f;

- (CGRect)rectForDismissedState:(id)transitionContext
{
    UIViewController *fromViewController;
    UIView *containerView = [transitionContext containerView];
    
    if (self.presenting)
        fromViewController = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
    else
        fromViewController = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
    
    switch (fromViewController.interfaceOrientation)
    {
        case UIInterfaceOrientationLandscapeRight:
            return CGRectMake(-PresentedViewHeightLandscape, 0,
                              PresentedViewHeightLandscape, containerView.bounds.size.height);
        case UIInterfaceOrientationLandscapeLeft:
            return CGRectMake(containerView.bounds.size.width, 0,
                              PresentedViewHeightLandscape, containerView.bounds.size.height);
        case UIInterfaceOrientationPortraitUpsideDown:
            return CGRectMake(0, -PresentedViewHeightPortrait,
                              containerView.bounds.size.width, PresentedViewHeightPortrait);
        case UIInterfaceOrientationPortrait:
            return CGRectMake(0, containerView.bounds.size.height,
                              containerView.bounds.size.width, PresentedViewHeightPortrait);
        default:
            return CGRectZero;
    }
}


- (CGRect)rectForPresentedState:(id)transitionContext
{
    UIViewController *fromViewController;
    if (self.presenting)
        fromViewController = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
    else
        fromViewController = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
    
    switch (fromViewController.interfaceOrientation)
    {
        case UIInterfaceOrientationLandscapeRight:
            return CGRectOffset([self rectForDismissedState:transitionContext], PresentedViewHeightLandscape, 0);
        case UIInterfaceOrientationLandscapeLeft:
            return CGRectOffset([self rectForDismissedState:transitionContext], -PresentedViewHeightLandscape, 0);
        case UIInterfaceOrientationPortraitUpsideDown:
            return CGRectOffset([self rectForDismissedState:transitionContext], 0, PresentedViewHeightPortrait);
        case UIInterfaceOrientationPortrait:
            return CGRectOffset([self rectForDismissedState:transitionContext], 0, -PresentedViewHeightPortrait);
        default:
            return CGRectZero;
    }
}

See the demo here: https://vimeo.com/85142329

Finally

You can download the entire example project from https://github.com/brightec/CustomViewControllerTransition

I hope this has been useful, hopefully Apple will document these implementation details in the future.

Here are a few links on custom UIViewController transitions that I found helpful:

http://www.objc.io/issue-5/view-controller-transitions.html

http://www.teehanlax.com/blog/custom-uiviewcontroller-transitions/

http://blog.spacemanlabs.com/2013/11/custom-view-controller-transitions-in-landscape/

http://ios-blog.co.uk/tutorials/ios-7-custom-transitions/

This article was originally written for Brightec by Cameron Cooke

Top