13

I'm having a UISplitViewController that contains a UITabBarController as master view. This UITabBarController contains a UINavigationController. The detail view contains a UINavigationController as well.

Storyboard

On the iPad this works as expected. The show detail segue presents the imageview within the navigation controller on the detail view.

On the iPhone on the other hand I expected that the show detail segue pushes the detail view on the stack of the navigation controller of the master view. But actually it is presented modally over the master view.

When removing the UITabBarController from the storyboard and using the UINavigationController directly in the master view this works.

Has anybody an idea how I could present the detail view on the stack of the master's UINavigationController on an iPhone?

Peter Oettl
  • 468
  • 3
  • 10

5 Answers5

9

The problem with Peter's solution is that it will fall apart with the iPhone 6 +. How so? With that code, if an iPhone 6 + is in portrait orientation - the detail view pushes onto the navigation stack. All is well, so far. Now, rotate into landscape, and then you'll have the detail view showing as the detail view and the master view.

You'll need the split view controller's delegate to implement two methods:

- (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;
}

And now, you'll need to return the top view controller from the selected tab's navigation controller:

- (UIViewController*)splitViewController:(UISplitViewController *)splitViewController separateSecondaryViewControllerFromPrimaryViewController:(UIViewController *)primaryViewController
{
    UITabBarController *masterVC = splitViewController.viewControllers[0];

    if ([(UINavigationController*)masterVC.selectedViewController viewControllers].count > 1)
        return [(UINavigationController*)masterVC.selectedViewController popViewControllerAnimated:NO];
    else
        return nil; // Use the default implementation
}

With this solution, everything pushes onto the navigation stack when it should and also updates the detail view correctly on the iPad/6+ landscape.

Dreaming In Binary
  • 1,197
  • 6
  • 8
  • Your implementation of - (BOOL)splitViewController:(UISplitViewController *)splitViewController showDetailViewController:(UIViewController *)detailVC sender:(id)sender seems to be sufficient for me. – Leszek Zarna Mar 09 '15 at 10:33
8

I figured out how to put the detail on to the master's UINavigationController instead of presenting it modally over the UITabBarController.

Using the UISplitViewControllerDelegate method

- splitViewController:showDetailViewController:sender:

In case the UISplitViewController is collapsed get the masters navigation controller and push the detail view onto this navigation controller:

- (BOOL)splitViewController:(UISplitViewController *)splitViewController
   showDetailViewController:(UIViewController *)vc
                     sender:(id)sender {
    NSLog(@"UISplitViewController collapsed: %d", splitViewController.collapsed);

    // TODO: add introspection
    if (splitViewController.collapsed) {
        UITabBarController *master = (UITabBarController *) splitViewController.viewControllers[0];
        UINavigationController *masterNavigationController = (UINavigationController *)master.selectedViewController;

        // push detail view on the navigation controller
        //[masterNavigationController pushViewController:vc animated:YES];
        // push was not always working (see discussion in answer below), use showViewController instead
        [masterNavigationController showViewController:vc sender:sender];

        return YES;
    }

    return NO;
}
Peter Oettl
  • 468
  • 3
  • 10
  • I have the exact same storyboard and problem, and your answer got me a little closer to the goal. However, the splitViewController:showDetailViewController:sender: delegate is only firing once for me...subsequent "shows" don't trigger the delegate. What did you end up assigning as the delegate? – Fozzle Nov 14 '14 at 21:25
  • Further investigation shows that my splitView delegate is being reassigned after the navigation. If I figure out why I'll update. – Fozzle Nov 14 '14 at 22:05
  • 2
    This solution won't work for the iPhone 6 + though. Start your app in portrait, go to the detail view. Then change to landscape, both your detail and master views will show the detail view. – Jason Renaldo Jan 03 '15 at 04:51
4

The answer of @PeterOettl to his own question put me on the right way and is great for that. So the credit belongs to him.

I have nearly the same storyboard structure as him, but as vc is a navigationController I get a runtime error saying

'Pushing a navigation controller is not supported'

As said, that is because vc is the navigationController of the detail view and not the viewController of the detail view.

Note that I am surprised that @PeterOettl does not get that error in his case also, as the segue given in the storyboard picture, points to the navigation controller of the detail view.

Therefore the code should like that (in Swift) simply adding

let detailViewControllerNavigationController = (vc as UINavigationController).viewControllers[0] as UIViewController

and pushing detailViewControllerNavigationController instead of vc

and the whole code is

func splitViewController(splitViewController: UISplitViewController, showDetailViewController vc: UIViewController, sender: AnyObject?) -> Bool {
    println("UISplitViewController collapsed: \(splitViewController.collapsed)")
    if (splitViewController.collapsed) {
        let master = splitViewController.viewControllers[0] as UITabBarController
        let masterNavigationController = master.selectedViewController as UINavigationController

        let detailViewControllerNavigationController = (vc as UINavigationController).viewControllers[0] as UIViewController

        masterNavigationController.pushViewController(detailViewControllerNavigationController, animated: true)

        return true
    } else {
        return false
    }
}

Also note that this code is put in the AppDelegate.swift of the master-detail example of Xcode where a tab bar is added in the master view.

EDIT

In the comments we discussed with @PeterOettl of the difference between .pushViewController and .showViewController.

Apple documentation says :

showViewController:sender:

This method pushes a new view controller onto the navigation stack in a similar way as the pushViewController:animated: method. You can call this method directly if you want but typically this method is called from elsewhere in the view controller hierarchy when a new view controller needs to be shown.

Available in iOS 8.0 and later.

HpTerm
  • 8,151
  • 12
  • 51
  • 67
  • 1
    Thats really interesting! When writing my solution above I only implemented the upper show detail segue from my story board. When implementing the lower show detail segue now, I received the same error ('Pushing a navigation controller is not supported') as you did. I was not able to figure out any differences between those two segues, but your hint worked well. Thanks for that. Anyway, it would be really interesting to find out why it works in the first segue, but not in the second. If anybody has an idea to this it would be great. – Peter Oettl Oct 04 '14 at 11:33
  • 1
    instead of pushViewController I used the method showViewController. This works well in both cases, even though I'm still unsure about the differences as both times I was pushing a UINavigationController on a UINavigationController. Can you try if showViewController works in your case as well? – Peter Oettl Oct 05 '14 at 10:05
  • 1
    Sorry for the delay. I tried with .showViewController instead of .pushViewController and, same as you, I don't see any differences. The animation looks the same and everything looks the same. I am asking myself if the frame word identifies that it show a navigation controller and therefore automatically pushes on show. – HpTerm Oct 09 '14 at 07:04
  • 1
    I found the answer. Apple documentation says : showViewController:sender: This method pushes a new view controller onto the navigation stack in a similar way as the pushViewController:animated: method. You can call this method directly if you want but typically this method is called from elsewhere in the view controller hierarchy when a new view controller needs to be shown. Available in iOS 8.0 and later. – HpTerm Oct 09 '14 at 07:08
1

I appreciate this discussion thread when I was implementing exactly the same UI structure app, and furthurmore made it adaptive for iPhone 6 Plus rotation and iPad multitasking (Slide Over/Split View, iOS 9 or later).

We have put the full solution (adaptive UISplitViewController with UITabBarController as primary view controller) open sourced on GitHub indievox-inc/TabBarSplitViewController. Thanks!

denkeni
  • 969
  • 1
  • 10
  • 22
0

I implemented @Dreaming In Binary's answer in Swift:

func splitViewController(splitViewController: UISplitViewController, showDetailViewController vc: UIViewController, sender: AnyObject?) -> Bool {
    let masterVC = splitViewController.viewControllers[0] as UITabBarController

    if splitViewController.traitCollection.horizontalSizeClass == .Compact {
        masterVC.selectedViewController?.showViewController(vc, sender: sender)
    } else {
        splitViewController.viewControllers = [masterVC, vc]
    }

    return true
}

func splitViewController(splitViewController: UISplitViewController, separateSecondaryViewControllerFromPrimaryViewController primaryViewController: UIViewController!) -> UIViewController? {
    let masterVC = splitViewController.viewControllers[0] as UITabBarController

    if let navController = masterVC.selectedViewController as? UINavigationController {
        if navController.viewControllers.count > 1 {
            return navController.popViewControllerAnimated(false)
        }
    }
    return nil
}
Community
  • 1
  • 1