I am subclassing a UINavigationController
to serve as the main root controller of my application. The purpose of the view controller is to present a custom splash screen that allows for a swipe action from (right to left) to enter the main application. The custom transition fades the splash view during the swipe and reveals the main application underneath (a UITabBarController
). Most of my code is following the objc.io article on custom view controller transitions in iOS7: http://www.objc.io/issue-5/view-controller-transitions.html.
Everything seems to work as expected. However, very rarely, I am getting reports of a black screen appearing once the splash screen disappears, instead of the main screen. I have been unable to reproduce the behavior. Here is the code:
First, setting up the main root view controller in the custom app delegate:
- (void)applicationDidFinishLaunching:(UIApplication *)application
{
self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
self.rootViewController = [[CustomRootViewController alloc] initWithNavigationBarClass:nil toolbarClass:nil];
self.window.rootViewController = self.rootViewController;
[self.window makeKeyAndVisible];
}
Important portions from CustomRootViewController
(note I'm hiding the navigation bar):
- (instancetype) initWithNavigationBarClass:(Class)navigationBarClass toolbarClass:(Class)toolbarClass
{
self = [super initWithNavigationBarClass:navigationBarClass toolbarClass:toolbarClass];
if (self) {
self.navigationBarHidden = YES;
}
return self;
}
- (void)viewDidLoad
{
[super viewDidLoad];
self.delegate = self;
// This is the main UI for the app
UIStoryboard *mainStoryboard = [UIStoryboard storyboardWithName:@"Main" bundle:[NSBundle mainBundle]];
CustomTabBarControllerViewController *mainViewController = [mainStoryboard instantiateInitialViewController];
self.mainViewController = mainViewController;
UIStoryboard *splashStoryboard = [UIStoryboard storyboardWithName:@"Splash" bundle:[NSBundle mainBundle]];
SplashViewController *splashViewController = [splashStoryboard instantiateInitialViewController];
self.splashViewController = splashViewController;
// Initialize the navigation controller to have the main app sitting under the splash screen
self.viewControllers = @[self.mainViewController, self.splashViewController];
}
// In the public interface, called from the Splash screen when the user can perform the action
- (void)allowSwipeToDismiss
{
UIPanGestureRecognizer *panGestureRecognizer = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(handlePan:)];
panGestureRecognizer.delegate = self;
[self.view addGestureRecognizer:panGestureRecognizer];
}
- (id<UIViewControllerAnimatedTransitioning>)navigationController:(UINavigationController *)navigationController
animationControllerForOperation:(UINavigationControllerOperation)operation
fromViewController:(UIViewController *)fromVC
toViewController:(UIViewController *)toVC
{
if (operation == UINavigationControllerOperationPop) {
return [[CustomRootViewControllerAnimator alloc ] init];
}
return nil;
}
- (id<UIViewControllerInteractiveTransitioning>)navigationController:(UINavigationController *)navigationController
interactionControllerForAnimationController:(id<UIViewControllerAnimatedTransitioning>)animationController
{
return self.interactiveController;
}
- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer
{
// Only enable swiping when on the Swipe To Enter screen
// After we are in the main app, we don't want this gesture recognizer interfering with the rest of the app
if (self.topViewController == self.splashViewController) {
return true;
} else {
return false;
}
}
- (void)handlePan:(UIPanGestureRecognizer *)recognizer
{
if (recognizer.state == UIGestureRecognizerStateBegan) {
self.interactiveController = [[UIPercentDrivenInteractiveTransition alloc] init];
[self popViewControllerAnimated:YES];
} else if (recognizer.state == UIGestureRecognizerStateChanged) {
CGPoint translation = [recognizer translationInView:self.view];
CGFloat viewWidth = self.view.bounds.size.width;
CGFloat percentDone = -translation.x / viewWidth;
[self.interactiveController updateInteractiveTransition:percentDone];
} else if (recognizer.state == UIGestureRecognizerStateEnded) {
if (self.interactiveController.percentComplete < 0.5) {
[self.interactiveController cancelInteractiveTransition];
} else {
[self.interactiveController finishInteractiveTransition];
}
self.interactiveController = nil;
}
}
And here is the code for the CustomRootViewControllerAnimator
being returned by my custom UINavigationController:
@implementation CustomRootViewControllerAnimator
- (NSTimeInterval)transitionDuration:(id<UIViewControllerContextTransitioning>)transitionContext
{
return 1;
}
- (void)animateTransition:(id<UIViewControllerContextTransitioning>)transitionContext
{
UIViewController *toViewController = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
UIViewController *fromViewController = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
[[transitionContext containerView] insertSubview:toViewController.view
belowSubview:fromViewController.view];
[UIView animateWithDuration:[self transitionDuration:transitionContext] animations:^{
CGFloat screenWidth = [[UIScreen mainScreen] bounds].size.width;
fromViewController.view.transform = CGAffineTransformMakeTranslation(-screenWidth, 0);
fromViewController.view.alpha = 0;
} completion:^(BOOL finished) {
fromViewController.view.transform = CGAffineTransformIdentity;
[transitionContext completeTransition:![transitionContext transitionWasCancelled]];
}];
}
@end