Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

iOS custom transitions and rotation

I'm using custom transitions to display a fullscreen modal game view. When the user starts a game, the root view controller scales down "into" the screen, while the fullscreen game view controller zooms from a larger scale down onto the display while transitioning from 0% opacity to 100% opacity. Simple!

The transition looks great and works fine, also correctly reversing the animation when dismissing the game view controller.

The issue I'm having is that if the device is rotated while the fullscreen game view controller is displayed, on returning to the root view controller, the layout is all screwy. And further rotation doesn't fix the problem, the layout is screwy and stays screwy.

If I disable the use of a custom transition, this problem is gone. Also, if I keep the custom transition, but disable calls setting a CATransform3D on my source and destination views in the transition animation, the problem goes away again.

Here's my transitioning delegate:

class FullscreenModalTransitionManager: NSObject, UIViewControllerAnimatedTransitioning, UIViewControllerTransitioningDelegate  {

    private var presenting:Bool = true

    // MARK: UIViewControllerAnimatedTransitioning protocol methods

    func animateTransition(transitionContext: UIViewControllerContextTransitioning) {

        // get reference to our fromView, toView and the container view that we should perform the transition in
        let container = transitionContext.containerView()
        let fromView = transitionContext.viewForKey(UITransitionContextFromViewKey)!
        let toView = transitionContext.viewForKey(UITransitionContextToViewKey)!

        let scale:CGFloat = 1.075
        //let bigScale = CGAffineTransformMakeScale(scale, scale)
        //let smallScale = CGAffineTransformMakeScale(1/scale,1/scale)
        let bigScale = CATransform3DMakeScale(scale, scale, 1)
        let smallScale = CATransform3DMakeScale(1/scale, 1/scale, 1)
        let smallOpacity:CGFloat = 0.5
        let presenting = self.presenting

        if presenting {

            // when presenting, incoming view must be on top
            container.addSubview(fromView)
            container.addSubview(toView)

            toView.layer.transform = bigScale
            toView.opaque = false
            toView.alpha = 0

            fromView.layer.transform = CATransform3DIdentity
            fromView.opaque = false
            fromView.alpha = 1
        } else {

            // when !presenting, outgoing view must be on top
            container.addSubview(toView)
            container.addSubview(fromView)

            toView.layer.transform = smallScale
            toView.opaque = false
            toView.alpha = smallOpacity

            fromView.layer.transform = CATransform3DIdentity
            fromView.opaque = false
            fromView.alpha = 1
        }


        let duration = self.transitionDuration(transitionContext)

        UIView.animateWithDuration(duration,
            delay: 0.0,
            usingSpringWithDamping: 0.7,
            initialSpringVelocity: 0,
            options: nil,
            animations: {

                if presenting {
                    fromView.layer.transform = smallScale
                    fromView.alpha = smallOpacity
                } else {
                    fromView.layer.transform = bigScale
                    fromView.alpha = 0
                }

            },
            completion: nil )

        UIView.animateWithDuration(duration,
            delay: duration/6,
            usingSpringWithDamping: 0.7,
            initialSpringVelocity: 0.5,
            options: nil,
            animations: {
                toView.layer.transform = CATransform3DIdentity
                toView.alpha = 1
            },
            completion: { finished in
                transitionContext.completeTransition(true)
            })
    }

    func transitionDuration(transitionContext: UIViewControllerContextTransitioning) -> NSTimeInterval {
        return 0.5
    }

    // MARK: UIViewControllerTransitioningDelegate protocol methods

    func animationControllerForPresentedController(presented: UIViewController, presentingController presenting: UIViewController, sourceController source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
        self.presenting = true
        return self
    }

    func animationControllerForDismissedController(dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
        self.presenting = false
        return self
    }
}

And, for visual reference, here's what my root view controller normally looks like:

Correct layout for my root view controller

And, for visual reference, here's what my root view controller looks like if I rotate my device (at least once) while in the game view, and return to the root view controller.

Broken layout, after rotating device while in fullscreen game view and returning to root view

And a final note - just in case it rings any bells - I'm using autolayout and size classes to lay out my root view controller.

Thanks,

like image 583
TomorrowPlusX Avatar asked Aug 12 '15 15:08

TomorrowPlusX


3 Answers

I had a similar problem, I had written a custom transition for push and pop on a navigation controller. If you rotated the device after you did a push, when you popped back to the root view controller it's frame would be unchanged.


Problem

enter image description here


Solution

Objective C

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

    // ViewController Reference
    UIViewController *toViewController = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];

    // Fix layout bug in iOS 9+
    toViewController.view.frame = [transitionContext finalFrameForViewController:toViewController];

    // The rest of your code ...
} 

Swift 3.0

func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {

    // ViewController reference
    let toViewController = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.to)!

    // Fix layout bug in iOS 9+
    toViewController.view.frame = transitionContext.finalFrame(for: toViewController)

    // The rest of your code ...
}
like image 135
agilityvision Avatar answered Nov 15 '22 09:11

agilityvision


I accidentally figured this out, while doing some manual layout code on views with non-identity transforms. Turns out if your view has a non-identity transform, normal layout code fails.

The fix, in my transitioning delegate was to take the transited-out view, and in the animation complete callback set its transform to identity (since that view's invisible at the end of the animation, and behind the new view, this has no effect on the appearance)

UIView.animateWithDuration(duration,
    delay: 0.0,
    usingSpringWithDamping: 0.7,
    initialSpringVelocity: 0,
    options: nil,
    animations: {

        if presenting {
            fromView.transform = smallScale
            fromView.alpha = smallOpacity
        } else {
            fromView.transform = bigScale
            fromView.alpha = 0
        }

    },
    completion: { completed in
        // set transform of now hidden view to identity to prevent breakage during rotation
        fromView.transform = CGAffineTransformIdentity
    })
like image 5
TomorrowPlusX Avatar answered Nov 15 '22 10:11

TomorrowPlusX


Finally I found solution for this issue. You need to improve @agilityvision code. You need to add BOOL value, something like closeVCNow that indicates you want to close VC, and in animateTransition:, in animation block do that:

if (self.closeVCNow) {
      toVC.view.transform = CGAffineTransformIdentity; 
      toVC.view.frame = [transitionContext finalFrameForViewController:toVC];
      self.closeVCNow = NO;
  }
like image 1
daleijn Avatar answered Nov 15 '22 08:11

daleijn