18

I have a custom animated UIViewController transition, and it seems that there is a bug in iOS that screws up the layout in landscape orientation. In the main animation method, i'm given a mix of landscape and portrait views. (In portrait the views are all portrait, so no problem.)

- (void)animateTransition:(id <UIViewControllerContextTransitioning>)transitionContext;
{
  UIViewController* toViewController = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
  UIViewController* fromViewController = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
  UIView *containerView = [transitionContext containerView];

  // fromViewController.view => landscape, transform
  // toViewController.view => portrait, transform
  // containerView => portrait, no transform

  [containerView addSubview:toViewController.view];

  // ...animation... //
}

I know that the frame property is not reliable when a view has a transform - so I'm guessing this is the root of the problem. In landscape mode, the to/from viewControllers views have a 90 deg clockwise transform [0 -1 1 0]. I've tried using bounds/center to size and position the view, as well removing the transform and then reapplying it, but UIKit fights me and insists on displaying the view as portrait. Annoying!

In the screenshot, the dark grey is the UIWindow background, and the red is the added modal view controller which should cover the whole screen.

the red view is in portrait

Anyone found a workaround?

Jason Moore
  • 7,169
  • 1
  • 44
  • 45
  • Hi Jason, I have had this issue for a while now and I really don't think that the accepted answer is an acceptable solution as it seems hacky and I haven't got it to work on iOS 7.1. That being said I have answered the question to address the issue properly without hacks. Please let me know if this is helpful – Daniel Galasko Jul 17 '14 at 11:52

5 Answers5

17

Ok, the fix is surprisingly simple:

Set the toViewController frame to the container before adding the view to the container.

toViewController.view.frame = containerView.frame;
[containerView addSubview:toViewController.view];

Update: There is still a limitation in that you don't know the orientation of the frame. It is portrait initially, but stretched into landscape when it is displayed on screen. If you wanted to slide in the view from the right, in landscape it might slide in from the "top" (or the bottom if viewing the other landscape!)

Jason Moore
  • 7,169
  • 1
  • 44
  • 45
  • Lovin this question and answer, but damn the transform / slide in from the wrong direction is a pain in the neck – Alfie Hanssen Mar 26 '14 at 15:22
  • consider accepting my answer above? I've solved the remaining piece of the puzzle related to the transform applied to the view, and uploaded the solution to Github. – Alfie Hanssen Apr 08 '14 at 18:37
  • Maybe better to use: toVC.view.frame = transitionContext.finalFrameForViewController(toVC) – Cedrick Jul 04 '16 at 09:54
12

I came across this issue and I just don't feel that the above solutions do this any justice. I propose a solution that doesn't require hacky code and hard coded frames.

UIView has an awesome function to convert a CGRect into the coordinate space of another (namely; +[UIView convertRect:fromView:]). So I want to detail a far simpler way one can achieve this effect in any orientation without any hardcoded values. In this example lets say we want a simple animation that slides a view in from the right of the screen.

So in our animator's animateTransition(:) we could simply perform the following:

Swift

func animateTransition(transitionContext: UIViewControllerContextTransitioning) {
    let toViewController = transitionContext.viewControllerForKey(UITransitionContextToViewControllerKey)!
    let fromViewController = transitionContext.viewControllerForKey(UITransitionContextFromViewControllerKey)!
    
    let toView = toViewController.view
    let fromView = fromViewController.view
    let containerView = transitionContext.containerView()
    
    if(isPresenting) {
        //now we want to slide in from the right
        let startingRect = CGRectOffset(fromView.bounds, CGRectGetWidth(fromView.bounds), 0)
        toView.frame = containerView.convertRect(startingRect, fromView:fromView);
        containerView.addSubview(toView)
        let destinationRect = containerView.convertRect(fromView.bounds, fromView: fromView)
        UIView.animateWithDuration(transitionDuration(transitionContext),
            delay: 0,
            usingSpringWithDamping: 0.7,
            initialSpringVelocity: 0.7,
            options: .BeginFromCurrentState,
            animations: { () -> Void in
                toView.frame = destinationRect
        }, completion: { (complete) -> Void in
            transitionContext.completeTransition(!transitionContext.transitionWasCancelled())
        })
    } else {
        //we want to slide out to the right
        let endingRect = containerView.convertRect(CGRectOffset(fromView.bounds, CGRectGetWidth(fromView.bounds), 0), fromView: fromView)
        UIView.animateWithDuration(transitionDuration(transitionContext),
            delay: 0,
            usingSpringWithDamping: 0.7,
            initialSpringVelocity: 0.7,
            options: .BeginFromCurrentState,
            animations: { () -> Void in
                fromView.frame = endingRect
            }, completion: { (complete) -> Void in
                if !transitionContext.transitionWasCancelled() {
                    fromView.removeFromSuperview()
                }
                transitionContext.completeTransition(!transitionContext.transitionWasCancelled())
        })
    }
}

Objective-C

UIViewController* toViewController = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
UIViewController* fromViewController = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
    
UIView *toView = toViewController.view;
UIView *fromView = fromViewController.view;
UIView *containerView = [transitionContext containerView];

if(self.isPresenting) {
    //now we want to slide in from the right
    CGRect startingRect = CGRectOffset(fromView.bounds, CGRectGetWidth(fromView.bounds), 0);
    toView.frame = [containerView convertRect:startingRect fromView:fromView];
    [containerView addSubview:toView];
    [UIView animateWithDuration:[self transitionDuration:transitionContext]
                     animations:^{
                         toView.frame = [containerView convertRect:fromView.bounds
                                                          fromView:fromView];
                     }
                     completion:^(BOOL finished) {
                         [transitionContext completeTransition:![transitionContext transitionWasCancelled]];
                     }];

} else {
    //we want to slide out to the right
    [UIView animateWithDuration:[self transitionDuration:transitionContext]
                     animations:^{
                         CGRect endingRect = CGRectOffset(fromView.bounds, CGRectGetWidth(fromView.bounds), 0);
                         fromView.frame = [containerView convertRect:endingRect fromView:fromView];
                     }
                     completion:^(BOOL finished) {
                         [fromView removeFromSuperview];
                         [transitionContext completeTransition:![transitionContext transitionWasCancelled]];
                     }];

}

I hope this helps someone else who came here in the same boat (if it does, an up-vote won't hurt :) )

Community
  • 1
  • 1
Daniel Galasko
  • 23,617
  • 8
  • 77
  • 97
8

The existing answer goes part way but not all the way (we want proper frames and rotation handling on both devices, all orientations, for both animated and interactive transitions).

This blog post helps:

http://www.brightec.co.uk/blog/ios-7-custom-view-controller-transitions-and-rotation-making-it-all-work

And it quotes an Apple Support person stating the true nature of the problem:

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

But it doesn't address interactive transitions.

I worked out a complete solution to the problem here:

https://github.com/alfiehanssen/Cards

Essentially, you need to calculate the frames of your viewControllers based on the orientation of one of the viewControllers (toViewController or fromViewController) rather than the bounds of the transitionContext's containerView.

Community
  • 1
  • 1
Alfie Hanssen
  • 16,964
  • 12
  • 68
  • 74
  • Adding a UIImageView instance to the container view during a transition also ends up with undesired behaviour. The frame is correct, yet the image's orientation is aligned with the portrait format of the device. I have found that by using UINavigationController-based transitions instead the container view's frame and orientation is correct. – Alex Apr 08 '14 at 14:29
  • for a quick and dirty solution to the landscape mode issue; apply a "code transform". I.e., whenever you use x, use y, and instead of width, use height, etc. – Max MacLeod Apr 30 '14 at 12:20
  • Right on, that's effectively what's happening [here](https://github.com/alfiehanssen/Cards/blob/master/Cards/Transitions/TransitionUtilities.h) in the repo linked in my answer above. – Alfie Hanssen May 02 '14 at 00:02
  • My bad, it's public now – Alfie Hanssen May 06 '14 at 02:57
  • Please see my answer regarding this issue, you don't need to hardcode the frame values you can use the convertrect:fromView: method. Cleaner and more robust. – Daniel Galasko Jul 23 '14 at 09:54
3

I was stumped with this issue as well. I didn't like the switch/case solution too much. I ended up creating this function instead:

@implementation UIView (Extras)

- (CGRect)orientationCorrectedRect:(CGRect)rect {
    CGAffineTransform ct = self.transform;

    if (!CGAffineTransformIsIdentity(ct)) {
        CGRect superFrame = self.superview.frame;
        CGPoint transOrigin = rect.origin;
        transOrigin = CGPointApplyAffineTransform(transOrigin, ct);

        rect.origin = CGPointZero;
        rect = CGRectApplyAffineTransform(rect, ct);

        if (rect.origin.x < 0.0) {
            transOrigin.x = superFrame.size.width + rect.origin.x + transOrigin.x;
        }
        if (rect.origin.y < 0.0) {
            transOrigin.y = superFrame.size.height + rect.origin.y + transOrigin.y;
        }
        rect.origin = transOrigin;
    }

    return rect;
}

- (CGRect)orientationCorrectedRectInvert:(CGRect)rect {
    CGAffineTransform ct = self.transform;

    if (!CGAffineTransformIsIdentity(ct)) {
        ct = CGAffineTransformInvert(ct);

        CGRect superFrame = self.superview.frame;
        superFrame = CGRectApplyAffineTransform(superFrame, ct);
        CGPoint transOrigin = rect.origin;
        transOrigin = CGPointApplyAffineTransform(transOrigin, ct);

        rect.origin = CGPointZero;
        rect = CGRectApplyAffineTransform(rect, ct);

        if (rect.origin.x < 0.0) {
            transOrigin.x = superFrame.size.width + rect.origin.x + transOrigin.x;
        }
        if (rect.origin.y < 0.0) {
            transOrigin.y = superFrame.size.height + rect.origin.y + transOrigin.y;
        }

            rect.origin = transOrigin;
        }

        return rect;
    }

Basically, you can create your frame rects using the portrait or landscape coordinates but run it through the function with the view's transform before applying it to the view. With this method, you can use bounds to get correct view size.

        CGRect endFrame = toViewController.view.frame;
        CGRect startFrame = endFrame;
        startFrame.origin.y = fromViewController.view.bounds.size.height;

        endFrame = [fromViewController.view orientationCorrectedRect:endFrame];
        startFrame = [fromViewController.view orientationCorrectedRect:startFrame];

        toViewController.view.frame = startFrame;
leechbite.com
  • 39
  • 1
  • 3
-1

One solution is to have a very short (or zero-second) transition, then once the transition is finished and your view controller is presented, it will have the correct transforms applied to it. You then perform your animations from within the presented view controller itself.

codeperson
  • 8,050
  • 5
  • 32
  • 51
  • NOTE: this is only relevant in iOS 7. In iOS 8, UIViewController transitions in landscape work as expected. – codeperson Nov 14 '14 at 18:35