Transitioning bugs

Important

This post attempts to work around one of the bugs triggered by setting modalPresentationStyle to Custom. However, there are numerous other bugs caused by this:

  • It doesn’t remove the ‘from’ view controller’s view from the window.
  • It doesn’t call will/did disappear on the ‘from’ view controller.
  • If you present a modal, rotate, then dismiss - the original view controller is wrongly rotated.
  • Unwind segues stop working.
  • The ‘to’ view is often nil in iOS8 and needs to be added post-completion (this is the bug described below).

This may sound huge, however there is one simple workaround: set the modalPresentationStyle to FullScreen. You still get the custom transition, and everything else just works. So, if you do that, you can ignore the rest of this post…

One more gotcha: On iOS7, the from view controller has a strong reference to your transition, so make sure that your transition references the view controllers weakly, or nil them just before calling completion, to avoid a memory leak.

Original post

Hi all, I struggled half of today on a bug in iOS8 when creating custom view controller transitions. Two of my friends shortly afterwards told me they had the same problem. In the hope that I can help someone out there not waste half a day on this problem, here we go:

The problem

Say you’ve got a View Controller, and you want to display it modally, but you want a more interesting transition than the normal ‘swoosh up, perform some action, tap close, and it swooshes down’. So you read the documentation, set modalPresentationStyle to Custom. Next, you set the modal VC’s transitioningDelegate to some class that implements UIViewControllerTransitioningDelegate (a reasonable choice is that your modal VC will be its own transitioning delegate). Then, on this delegate class, you implement animationControllerForPresentedController... and animationControllerForDismissedController... to return your presentation and dismissal transitions respectively. And then you implement those two transitions, following the basic rules: Add the ‘to’ view to the ‘container’ view at some stage, and call the context’s completeTransition when you’re done.

So far, so good, nothing overly complicated there (it sounds harder than it actually is). And for me, running it on iOS7 bears no surprises, it works nicely. However, on iOS8…

…on iOS8, on the dismissal transition, the ‘to’ view controller is removed from the main window at the end of the transition. This is an issue that has even tripped up geniuses such as Ash Furrow; His radar for this issue.

So I had to come up with a solution that would work on iOS7, iOS8, and (fingers crossed) on iOS9+ regardless of whether they fix the bug or not.

Solution

- (void)animateTransition:(id <UIViewControllerContextTransitioning>)transitionContext {
    // Get the 'from' and 'to' views/controllers.
    UIViewController *fromVC = [transitionContext viewControllerForKey:
        UITransitionContextFromViewControllerKey];
    UIViewController *toVC = [transitionContext viewControllerForKey:
        UITransitionContextToViewControllerKey];
    // viewForKey is only available on iOS8+.
    BOOL hasViewForKey = [transitionContext
        respondsToSelector:@selector(viewForKey:)];
    UIView *fromView = hasViewForKey ?
        [transitionContext viewForKey:UITransitionContextFromViewKey] :
        fromVC.view;
    UIView *toView = hasViewForKey ?
        [transitionContext viewForKey:UITransitionContextToViewKey] :
        toVC.view;
    UIView *container = [transitionContext containerView];
    
    // iOS8 has a bug where viewForKey:to returns nil.
    // The workaround is:
    // A) get the 'toView' from 'toVC'.
    // B) manually add the 'toView' to the container's
    // superview (eg the root window) after the completeTransition
    // call, as automatically happens on iOS7 where things work properly.
    BOOL toViewNilBug = toView==nil;
    if (!toView) { // Workaround by getting it from the view.
        toView = toVC.view;
    }
    UIView *containerSuper = container.superview;
    
    // Perform the transition.
    toView.frame = container.bounds;
    toView.alpha = 0;
    [container addSubView:toView];
    [UIView animateWithDuration:kDuration delay:0
        options:UIViewAnimationOptionCurveEaseIn animations:^{
        
        toView.alpha = 1;
        
    } completion:^(BOOL finished) {
    
        [transitionContext completeTransition:YES];

        if (toViewNilBug) {
            [containerSuper addSubview:toView];
        }
        
    }];
}

Explanation

I found that viewForKey:UITransitionContextToViewKey returns nil on iOS8. So if it’s nil, I grab the view from the ‘to’ view controller.

However, this bug also seems to result in the ‘to’ view not being moved from the container to the window when completeTransition:YES is called. On iOS7, before completeTransition is called, the ‘to’ view is a subview of the container; then completeTransition moves it up a level to be a subview of the container’s superview (which happens to be the UIWindow).

So if viewForKey:UITransitionContextToViewKey returns nil, I fail over to toVC.view and keep track of the fact that it hit the bug. Then immediately after the completeTransition call, if the bug was hit, I add it to the container’s initial superview (which happens to be the UIWindow), to mimic the iOS7 behaviour.

And we need to store the container’s original superview in the containerSuper variable, because container.superview returns nil after the completeTransition call. This is because the container is just temporary, and is correctly removed at the end of the transition by UIKit.

So this code works on iOS7 as well as iOS8, and should work on iOS9 regardless of whether they fix this bug or not. But don’t hold me to it.

Thanks for reading

If you’re interested, I recently released a video series on making an iOS app - please check it out: splinter.com.au/videos

I’d love to get an email if this helps you!

Thanks for reading! And if you want to get in touch, I'd love to hear from you: chris.hulbert at gmail.

Chris Hulbert

(Comp Sci, Hons - UTS)

Software Developer (Freelancer / Contractor) in Australia.

I have worked at places such as Google, Cochlear, Assembly Payments, News Corp, Fox Sports, NineMSN, FetchTV, Coles, Woolworths, Trust Bank, and Westpac, among others. If you're looking for help developing an iOS app, drop me a line!

Get in touch:
[email protected]
github.com/chrishulbert
linkedin



 Subscribe via RSS