0

I am using the Xcode 7 (though I think this applies to 6 also), master-detail template with a slight modification. The modification is to replace the master UINavigationController with a UITabBarController.

The UITabBarController contains a couple of UINavigationControllers which, in turn have segues to the Detail UINavigationController.

Working with the iPhone 6+ simulator, it works fine in landscape mode (non collapsed). When the UISplitViewController is collapsed, the app stops behaving as expected. Two things happen:

  • In AppDelegate's didFinishLaunchingWithOptions, the splitViewController.viewControllers array contains the UITabBarController (master) and UINavigationController (detail). Somewhere between didFinishLaunchingWithOptions and splitViewController:collapseSecondaryViewController:ontoPrimaryViewController the detail UINavigationController is removed from splitViewController.viewControllers. This doesn't happen if both master and detail are UINavigationController objects. It's interesting to note that, in Apple's documentation for UISplitViewController they say of the viewControllers property: "When the split view interface is expanded, this property contains two view controllers; when it is collapsed, this property contains only one view controller." The assertion in the documentation only seems to hold true after I modified the storyboard to have the UITabBarController as the master, in the default master-detail template, the assertion doesn't hold (i.e. there are always two viewControllers in the array regardless of whether the split view is collapsed or expanded).
  • If I add an item while running in collapsed mode, then the detail view is presented modally rather than being pushed onto the selected tab's navigation controller, so there is no way to get back to the master view. Rotating to landscape leads to a crash with EXC_BAD_ACCESS - for some reason I also get an error: "<Error>: CGImageCreate: invalid image size: 0 x 0.".

The only changes to the 'vanilla' template app at this stage are to replace the Master UINavigationController with a UITabBarController which contains UINavigationControllers, the storyboard is below:

enter image description here

I also commented out the following line in MasterViewController.m to prevent an earlier crash:

self.detailViewController = (DetailViewController *)[[self.splitViewController.viewControllers lastObject] topViewController];

My questions are:

  1. Why does the content of the viewControllers property change when I add a UITabBarController as master rather than leaving the UINavigationController in place? I ask because I think this is relevant in determining why the detail VC is presented modally rather than being pushed. I'm assuming that, since in collapsed mode, the SplitViewController behaves like a UINavigationController and there is some difficulty doing that when there is a non-UINavigationController between the UISplitViewController and the detail view.
  2. Following on from question 1, why does the detail controller get presented modally?

I'm seeking to understand what is going on as much as I am trying to find a correct way to get this to work if there is one (or more).

Further discussion and related questions

My question is very similar to this one: UINavigationController inside a UITabBarController inside a UISplitViewController presented modally on iPhone

I've had some success using a combination of the answers from 'Dreaming In Binary' and 'HpTerm'.

If I first take Dreaming In Binary's answer (using the code from that answer verbatim), then the app behaves almost the same as before; I observe the following in particular:

  • The detail view is still presented modally when selected from the portrait view; basically a slightly different path to presentation of the detail view is taken as we have added this method (taken from the aforementioned answer):

    -(BOOL)splitViewController:(UISplitViewController *)splitViewController showDetailViewController:(UIViewController *)detailVC sender:(id)sender
    {
      UITabBarController *masterVC = splitViewController.viewControllers[0];
    
      if (splitViewController.traitCollection.horizontalSizeClass == UIUserInterfaceSizeClassCompact)
        [masterVC.selectedViewController showViewController:detailVC sender:sender];
      else
        [splitViewController setViewControllers:@[masterVC, detailVC]];
    
      return YES;
    }
    

    showViewController still triggers a modal presentation of the detailViewController. There is no navigation button at the top right in portrait, but if you rotate the device to landscape, then the whole landscape is taken by the detail view, but we do see the leftBarButtonItem for the navigationItem being set with the splitViewController.displayModeButton item. The trouble is that the button has absolutely no effect other than that it shows the 'expand' button (for hiding the master view); if you press the button nothing happens to the view (the master view is already hidden anyway) but the button changes to the 'back' style which would normally allow you to show the master view again. In this case the 'back' button has no effect when you press it. Essentially you can toggle to button between 'expand' and 'back' but there is no other effect when pressing it. If you rotate back to landscape, then you once again have the modally presented detail view with no way to get back to the master view.

    • I still see the error <Error>: CGImageCreate: invalid image size: 0 x 0. but it is not accompanied by the crash with EXC_BAD_ACCESS.

So next up, is HpTerm's solution in Swift which, in my world looks like this:

- (BOOL)splitViewController:(UISplitViewController *)splitViewController showDetailViewController:(UIViewController *)detailViewController sender:(id)sender
{
    UITabBarController * masterViewController = splitViewController.viewControllers[0];

    if (splitViewController.traitCollection.horizontalSizeClass == UIUserInterfaceSizeClassCompact)
    {
        UIViewController * detailViewSubController = ((UINavigationController *)detailViewController).viewControllers[0];
        [((UINavigationController *)masterViewController.selectedViewController) pushViewController:(UIViewController *)detailViewSubController animated:NO];
      return YES;
    }
    else
    {
      return NO;
    }
}

So in the above, rather than trying to showViewController with the detail's UINavigationController, we extract the DetailViewController from the detail UINavigationController and actually push the DetailViewController onto the master UINavigationController. The reason for not pushing the detail UINavigationController onto the master UINavigationController is that an exception is thrown (applicable if any navigation controller is pushed onto another).

This actually yields an almost perfect result, but not quite. First, the good bits:

  • In portrait, everything works correctly we push the detail over the master on selection and when we navigate back, we get back to the master correctly.
  • On rotating to landscape, the split view displays master and detail correctly and we can show/hide the master view properly using the bar button item.

Now the bad:

  • On rotating back to portrait (with an item selected on the master side), we go to the master view when, in fact, we should be going to the selected detail item.

So the question here is, can the 'bad' point be corrected so that if an item is selected while in portrait, rotation back to landscape takes us to the selected detail item?

Apologies this is so long, and I salute you if you've made it this far! I'm open to suggestions on slimming the question down too, but wanted to supply sufficient detail to highlight exactly what is going on.

Community
  • 1
  • 1

2 Answers2

0

I'm still open to other answers as there are a few ways to solve this, but one way that worked is by modifying the Master-Detail App Template's splitViewController:collapseSecondaryViewController:ontoPrimaryViewController: implementation.

First some explanation; the implementation of splitViewController:showDetailViewController:sender: in the last part of the question above solved one (significant) problem on the UI side. It allowed the detail view to be pushed onto the master UINavigationController's stack while keeping the master UITabBarController visible and allowing navigation back to the master view. It did this in collapsed mode by taking the DetailViewController out of the split view controller's detail navigation controller and pushing the DetailViewController onto the stack. This is different from the default Master-Detail template as, in that version, the detail navigation controller is pushed over the master navigation controller and still works (I just haven't been able to find a way to make it work like this after adding the UITabBarController). Having done this, when we rotate to the landscape (expanded) view, everything still works correctly (the detail navigation controller comes back), but when we rotate to the portrait (collapsed) view, we end up back at the master view even when a detail item is selected.

I forced the push of the detail view (if there was data to display) by adding an additional condition to splitViewController:collapseSecondaryViewController:ontoPrimaryViewController: by triggering a segue to the detail view from the master:

if ([secondaryViewController isKindOfClass:[DetailViewController class]] && [(DetailViewController *)secondaryViewController detailItem] != nil && [primaryViewController isKindOfClass:[UITabBarController class]])
{
    UINavigationController * currentMasterNavigationController = ((UITabBarController *)primaryViewController).selectedViewController;
    MasterViewController * masterViewController = (MasterViewController *)[currentMasterNavigationController visibleViewController];
    [masterViewController performSegueWithIdentifier:@"showDetail" sender:masterViewController];
    return YES;
}

The modified function in full is:

- (BOOL)splitViewController:(UISplitViewController *)splitViewController collapseSecondaryViewController:(UIViewController *)secondaryViewController ontoPrimaryViewController:(UIViewController *)primaryViewController
{
    if ([secondaryViewController isKindOfClass:[UINavigationController class]] && [[(UINavigationController *)secondaryViewController topViewController] isKindOfClass:[DetailViewController class]] && ([(DetailViewController *)[(UINavigationController *)secondaryViewController topViewController] detailItem] == nil))
    {
        // Return YES to indicate that we have handled the collapse by doing nothing; the secondary controller will be discarded.
        return YES;
    }
    else if ([secondaryViewController isKindOfClass:[DetailViewController class]] && [(DetailViewController *)secondaryViewController detailItem] != nil && [primaryViewController isKindOfClass:[UITabBarController class]])
    {
        UINavigationController * currentMasterNavigationController = ((UITabBarController *)primaryViewController).selectedViewController;
        MasterViewController * masterViewController = (MasterViewController *)[currentMasterNavigationController visibleViewController];
        [masterViewController performSegueWithIdentifier:@"showDetail" sender:masterViewController];
        return YES;
    }
    return NO;
}

This does still seem too much of a hack to me, but it does work. I'd love to see an answer that is cleaner than this, which works more like the default template while still allowing for the UITabBarController

0

I believe you've got some point to the working implementation. It took me some weeks to get it done.

Apple's UISplitViewController does require some extra tweaks to get size class changing works correctly. I have put the adaptive UISplitViewController with UITabBarController as primary view controller open sourced on GitHub indievox-inc/TabBarSplitViewController, should be worthy of checking it out.

You may like to check the original implementation, which is simpler for porting to Objective-C code.

Key functions below:

 // MARK: to Compact Width size class (collapse)
    public func primaryViewControllerForCollapsingSplitViewController(splitViewController: UISplitViewController) -> UIViewController? {
        if let primaryTabViewController = splitViewController.viewControllers[0] as? UITabBarController,
               primaryNavViewController = primaryTabViewController.selectedViewController as? UINavigationController {
                let secondaryViewController = splitViewController.viewControllers[1]
                if !(secondaryViewController.dynamicType === DetailViewControllerType.Empty) {
                    dispatch_async(dispatch_get_main_queue()) {     // otherwise we may get console error "<Error>: CGImageCreate: invalid image size: 0 x 0."
                        primaryNavViewController.showViewController(secondaryViewController, sender: secondaryViewController)
                    }
                    return primaryTabViewController
                }
        }

        return nil
    }

// MARK: to Regular Width size class (separate/expand)
    public func splitViewController(splitViewController: UISplitViewController, separateSecondaryViewControllerFromPrimaryViewController primaryViewController: UIViewController) -> UIViewController? {
        if let primaryTabViewController = splitViewController.viewControllers[0] as? UITabBarController,
               primaryNavViewController = primaryTabViewController.selectedViewController as? UINavigationController {
                let primaryTopViewController = primaryNavViewController.topViewController
                if let primaryTopViewController = primaryTopViewController {
                    if ((primaryTopViewController.dynamicType === DetailViewControllerType.General)
                     || (primaryTopViewController.dynamicType === DetailViewControllerType.Empty)) {
                        primaryNavViewController.popViewControllerAnimated(false)
                        return primaryTopViewController
                    }
                }
        }

        return DetailViewControllerType.Empty.init()
    }

// MARK: override showDetailViewController
    public func splitViewController(splitViewController: UISplitViewController, showDetailViewController vc: UIViewController, sender: AnyObject?) -> Bool {
        let isCompactWidth = (splitViewController.traitCollection.horizontalSizeClass == UIUserInterfaceSizeClass.Compact)
        if isCompactWidth {
            if let primaryTabViewController = splitViewController.viewControllers[0] as? UITabBarController,
                      primaryViewController = primaryTabViewController.selectedViewController as? UINavigationController {
                        primaryViewController.showViewController(vc, sender: sender)
                        return true
            }
        }

        return false
    }
denkeni
  • 969
  • 1
  • 10
  • 22
  • It would be better to put the relevant code here, not on external sites (as great as Github is). – Fabio says Reinstate Monica Mar 31 '16 at 10:16
  • Sure, and I've added key functions to make the whole UISplitViewController work. Just need to read the documentation and test around to fully understand how UISplitViewControllerDelegate works. – denkeni Apr 04 '16 at 02:45