IOS8 SplitVC + TabBarController + NavigationController

I’m doing a universal App using size classes and I’m trying to use a SplitView with a TabBarController in the Master/Primary View. Before adding the splitView all worked fine, but now the App crashes (the reason depends on the hierarchy of the views).

So I tried the same storyboard starting from Apple SplitView template and add a TabBarController on its Master/primary view… same problem.

  • How to get audio volume level, and volume changed notifications on iOS?
  • Cancellable set of asynchronous operations with progress reporting in iOS
  • How can I open a Twitter tweet using the native Twitter app on iOS?
  • Selector to get indexPath UICollectionView Swift 3.0
  • How to define Outlets and Actions in the Classes Pane in Interface Builder in Xcode 4?
  • Swift regular expression format?
  • Hierarchy – Embedded master NavigationController in TabBarController:
    SplitVC (Master) > TabBarController > NavigationController > TableView
    SplitVC (Detail) > NavigationController > View

    Added this code in AppDelegate.m (as seen here stackoverflow questions ios8-tabbarcontroller… to prevent DetailView being presented modally):

    - (BOOL)splitViewController:(UISplitViewController *)splitViewController showDetailViewController:(UIViewController *)vc sender:(id)sender {
            NSLog(@"UISplitViewController collapsed: %d", splitViewController.collapsed);
    
        if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPhone)
        {
            if (splitViewController.collapsed) {
                UITabBarController *master = (UITabBarController *) splitViewController.viewControllers[0];
                UINavigationController *masterNavigationController = (UINavigationController *)master.selectedViewController;
                UINavigationController *destinationNavigationController = (UINavigationController *)vc;
    
                // push detail view on the navigation controller
                [masterNavigationController pushViewController:[destinationNavigationController.viewControllers lastObject] animated:YES];
    
                return YES;
            }
        }
    
        return NO;
    }
    

    It works fine… unless you simulate in iPhone6 Plus, in that case, after starting in portrait and selecting a row, if you rotate in landscape I see the detail view as primary AND secondary view.

    Without adding this code in portrait orientation with iPhones the detail view is presented modally and of course without navigation buttons.

    EDIT

    After different tries and with some external helps I’ve made some steps forward the solution.

    Short version (See Long Version to know why you have to do this)

    A correct solution to the problem is to subclass TabBarController and make it support some methods:

    @implementation MyTabBarController
    
    - (void)showViewController:(UIViewController *)vc sender:(id)sender
    {
        if ([self.selectedViewController isKindOfClass:UINavigationController.class])
            [self.selectedViewController showViewController:vc sender:sender];
        else
            [super showViewController:vc sender:sender];
    }
    
    - (UIViewController*)separateSecondaryViewControllerForSplitViewController:(UISplitViewController *)splitViewController
    {
        return [self.selectedViewController separateSecondaryViewControllerForSplitViewController:splitViewController];
    }
    
    - (void)collapseSecondaryViewController:(UIViewController *)secondaryViewController forSplitViewController:(UISplitViewController *)splitViewController
    {
        [self.selectedViewController collapseSecondaryViewController:secondaryViewController forSplitViewController:splitViewController];
    }
    

    Now I have a problem with viewControllers stack: with the iPhone6Plus (the only one supporting both horizontal regular and compact) the App crash if, when in landscape, you change tab without selecting a row (so the detailView remain the one for the previous tab) and then rotate in portrait.

    I know that I have to implement separation and collapse methods managing the views stacks properly but I can’t figure how. Can someone help on this?

    Long version (SplitViewController behaviour)

    Normally a split view controller and a navigation controller work
    together to ensure that a call to -showDetailViewController:sender:
    from a view controller that is contained within the split view
    controller results in the new detail view controller being pushed onto
    the navigation stack (when in a horizontally compact environment). To
    do this, UISplitViewController overrides
    -showDetailViewController:sender: and, if horizontally compact, calls its master view controller’s -showViewController:sender: method.
    UINavigationController overrides -showViewController:sender: and
    pushes the incoming view controller onto the navigation stack.

    UITabBarController however does not override
    -showViewController:sender: and so it inherits the default implementation which presents the incoming view controller modally.
    To work around this I have to subclass UITabBarController and override
    -showViewController:sender: to forward to the tab bar controller’s selectedViewController if the selectedViewController is a navigation
    controller.

    Furthermore, when a split view controller transitions from a compact
    to horizontal size class to a regular horizontal size class, the split
    view controller first sends a
    -splitViewController:separateSecondaryViewControllerFromPrimaryViewController:
    message to its delegate. The delegate can implement this method and
    handle the separation itself, returning the detail view controller.
    If the delegate does not implement this method, or if the
    implementation returns nil, the split view controller sends a
    -separateSecondaryViewControllerForSplitViewController: message to its primary view controller. The primary view controller should implement
    this method to handle the separation. The UINavigationController does
    implement -separateSecondaryViewControllerForSplitViewController:.
    It’s implementation pops the top view controller off the navigation
    stack and returns it. Because I am using a tab bar controller as the
    primary view controller, I must implement
    -separateSecondaryViewControllerForSplitViewController: and handle the separation by myself.

    Also I need to implement my own collapsing logic. When a split view
    controller transitions from a regular to horizontal size class to a
    compact horizontal size class, the split view controller first sends a
    -splitViewController:collapseSecondaryViewController:ontoPrimaryViewController:
    message to its delegate. The delegate can implement this method and
    handle the collapse itself. If the delegate does not implement this
    method, the split view controller sends a
    -collapseSecondaryViewController:forSplitViewController: message to its primary view controller. The primary view controller should
    implement this method to handle the separation.

    UINavigationController does implement
    -collapseSecondaryViewController:forSplitViewController:. It’s implementation pushes the secondary view controller onto the
    navigation stack. Because I am using a tab bar controller as the
    primary view controller, I must implement
    -collapseSecondaryViewController:forSplitViewController: and handle the collapse by myself.

    2 Solutions Collect From Internet About “IOS8 SplitVC + TabBarController + NavigationController”

    Try this snippet and told us your results. This snippet get from a website outside stackOverflow (Craig Marvelley)

    #pragma mark - Split view
    // Update secondaryview with the right screen
    - (UIViewController *)splitViewController:(UISplitViewController *)splitViewController separateSecondaryViewControllerFromPrimaryViewController:(UIViewController *)primaryViewController { 
    int tryIt = 0;
    
    if ((IS_IPHONE_6_PLUS) && (isLandscape)) {
        if ([primaryViewController isKindOfClass:[UINavigationController class]]) {
            for (UIViewController *controller in [(UINavigationController *)primaryViewController viewControllers]) {
                tryIt = tryIt + 1;
                if ([controller isKindOfClass:[UINavigationController class]] && ([[(UINavigationController *)controller visibleViewController] isKindOfClass:[yourPosibleScreen01 class]] || [[(UINavigationController *)controller visibleViewController] isKindOfClass:[yourPosibleScreen02 class]]) ) {
                    return controller;
                }
                // Sublevel where yo are to select the right screen. You must try with a number depends of how many internal hierarchy. But I believe you need number 2 but try it :) 
                if (tryIt > 2) {
                    return controller;
                }
            }
        }
        // Update detail screen
        UIViewController *toViewController = [self.storyboard instantiateViewControllerWithIdentifier:@"YourScreenToShow"];
        return toViewController;
    }
    return nil;
    }
    
    
     - (BOOL)splitViewController:(UISplitViewController *)splitViewController collapseSecondaryViewController:(UIViewController *)secondaryViewController ontoPrimaryViewController:(UIViewController *)primaryViewController {
    
         return NO;
    }
    #pragma mark - Split view
    

    So, I found something that works, even if is not the standard behaviour:

    - (void)collapseSecondaryViewController:(UIViewController *)secondaryViewController forSplitViewController:(UISplitViewController *)splitViewController
    {
        [self.selectedViewController.navigationController collapseSecondaryViewController:secondaryViewController forSplitViewController:splitViewController];
    }
    

    That is equivalent to return always YES in the splitViewController:collapseSecondaryViewController:ontoPrimaryViewController: delegate method. Like this you always discard the secondary controller.
    Hope this can help someone.