John Farkerson John Farkerson - 5 months ago 12
iOS Question

Creating a new navigation stack with a flip transition instead of pushing another controller

I had this code to make a flip transition between UITabControllers:

UIStoryboard *sb = [UIStoryboard storyboardWithName:@"OtherSb" bundle:nil];
PrimaryTabBarController *tabBarController = [sb instantiateInitialViewController];

[UIView transitionWithView:[APP_DELEGATE window]
duration:0.8
options:UIViewAnimationOptionTransitionFlipFromLeft
animations:^{
[[APP_DELEGATE window] setRootViewController:tabBarController];
[[APP_DELEGATE window] makeKeyAndVisible];
}
completion:nil];


However, strangely, during the flip transition, the tab bar briefly flashes from the bottom to the top of the screen. I was able to make that stop by doing the following:

PrimaryTabBarController *tabBarController = [sb instantiateInitialViewController];

tabBarController.modalTransitionStyle = UIModalTransitionStyleFlipHorizontal;
[self presentViewController:tabBarController animated:YES completion:nil];


The problem with this however is that I'm pushing another view controller onto the stack which can easily run through the memory. How can I create a new navigation stack without having the tab bar mess up the animation?

Answer

rootViewController is not very animatable property as you can imagine. makeKeyAndVisible is not animatable either and you should probably do that before you run any animations.

This API itself is very old (iOS 4.0) and I personally consider it more of a legacy of UIKit and I haven't seen it being used since iOS 6 when flipping views was still a thing.

Custom transitions introduced in iOS 7 is a very comfortable way of doing any kind of crazy animations however as you noticed, it creates a modal hierarchy that you don't always need.

All of these API were designed to work with sibling views within container. This is something mentioned in documentation and its samples. And it seems like that UIWindow is not suitable as global animation container.

Sample code from documentation:

[UIView transitionWithView:containerView
           duration:0.2
           options:UIViewAnimationOptionTransitionFlipFromLeft
           animations:^{ [fromView removeFromSuperview]; [containerView addSubview:toView]; }
           completion:NULL];

I suggest that you follow the same logic as custom transitions and first setup dummy root controller that will serve you as a container for animation.

Then you add your views or entire view controllers inside of it and run animation between sibling views using

+ transitionFromView:toView:duration:options:completion:`

or

- transitionFromViewController:toViewController:duration:options:animations:completion:

or

+ transitionWithView:duration:options:animations:completion:

In addition to that, there is a useful flag UIViewAnimationOptionShowHideTransitionViews that will automatically hide the flipped view to avoid it to flicker or re-appear after animation.

When animation is over, you can swap the entire root controller in one call, that should be unnoticed to user.

This API has some quirks too, for example if by any chance you use it when the app is not on screen or you run it on a window that is not currently visible, then it will simply swallow the call. I used to have a check like

if(fromViewController.view.window) { 
    /* run animations */ 
} else { 
    /* swap controllers without animations */ 
}

I made a sample project to demonstrate how to use temporary container view for transition

https://github.com/pronebird/FlipRootController

Sample category on UIWindow:

@implementation UIWindow (Transitions)

- (void)transitionToRootController:(UIViewController *)newRootController animationOptions:(UIViewAnimationOptions)options {
    // get references to controllers
    UIViewController *fromVC = self.rootViewController;
    UIViewController *toVC = newRootController;

    // setup transition view
    UIView *transitionView = [[UIView alloc] initWithFrame:self.bounds];

    // add subviews into transition view
    [transitionView addSubview:toVC.view];
    [transitionView addSubview:fromVC.view];

    // add transition view into window
    [self addSubview:transitionView];

    // flush any outstanding animations
    // UIButton may cancel transition if this method is called from touchUpInside, etc..
    [CATransaction flush];

    [UIView transitionFromView:fromVC.view
                        toView:toVC.view
                      duration:0.5
                       options:options
                    completion:^(BOOL finished) {
                        // set new root controller after animation
                        self.rootViewController = toVC;

                        // move VC's view out of transition view
                        [self addSubview:toVC.view];

                        // remove transition view
                        [transitionView removeFromSuperview];
                    }];
}

@end
Comments