IOS8 SplitVC + TabBarController + NavigationController
Asked Answered
E

2

4

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.

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.

Essayist answered 9/7, 2015 at 22:44 Comment(0)
E
1

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.

Essayist answered 16/8, 2015 at 22:30 Comment(0)
G
0

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
Gnni answered 16/7, 2015 at 2:59 Comment(4)
It doesn't work, same problem. Do you tried it? It work for you?Essayist
Hello @Zag, sorry but i forget a counter of levels. I don't know why, but sometimes (after change portrait to lands or viceversa) we have 3 screens and not just 2, I suppose is just for the order of internal action to shoe screen. The code above works for me.Gnni
Hi @Beto, AFAIK the SplitViewController hierarchy change depending on collapsed or separated mode. See this article. I'll try your solution. Thanks.Essayist
I hope this help you and if you found something better please write it here.Gnni

© 2022 - 2024 — McMap. All rights reserved.