16

In XCode 6, if you create a new project based on the Master-Detail Application template, you get a universal storyboard that is supposed to be good for all devices.

When selecting a cell in the master view, the detail view is updated via an adaptive "show detail" segue. On an iPhone 4, 5, 6 or 6+ in portrait, this segue will take the form of a push as expected. On an iPad or an iPhone 6+ in landscape, it will cause the detail view to be updated as expected.

Now, if you insert a UITabBarController as the master view controller which has a tab to the original master view controller, the adaptive segue that occurs when selecting a cell in the master view does not behave as expected on iPhones. Instead of getting a push transition, you now get a modal transition. How can I fix that? Seems odd that this is not supported by default.

I found the following post useful: iOS8 TabbarController inside a UISplitviewController Master But when using the suggested method, I don't get the right behaviour on an iPhone 6 Plus when I rotate to landscape after a push in portrait. The content of the detail view appears in the master view which is not surprising since that's what the suggested solution does.

Thanks!

Community
  • 1
  • 1
phamel
  • 373
  • 2
  • 8
  • Is the original master view controller embedded in a navigation controller (so the root view controller of the tab is the navigation controller)? – rdelmar Oct 05 '14 at 19:31
  • Yes, the root view controller of the tab is a navigation controller which in turn holds a table view controller. – phamel Oct 06 '14 at 20:49

3 Answers3

7

Re-watching videos from WWDC14 I think I've found a better answer.

  1. Use a custom UISplitViewController (subclass)
  2. Override the showDetailViewController operation
  3. Use the traitCollection to determine the class of the UISplitViewController
  4. If the horizontal class is Compact, get the navigationController to call showViewController

Here is the the code of the custom UISplitViewController :

import UIKit

class CustomSplitViewController: UISplitViewController {

    override func showDetailViewController(vc: UIViewController!, sender: AnyObject!) {

        if (self.traitCollection.horizontalSizeClass == UIUserInterfaceSizeClass.Compact) {
            if let tabBarController = self.viewControllers[0] as? UITabBarController {
                if let navigationController = tabBarController.selectedViewController as? UINavigationController {
                    navigationController.showViewController(vc, sender: sender)
                    return
                }
            }
        }

        super.showDetailViewController(vc, sender: sender)
    }
}

Do not forget to the set the custom class in the storyboard.

Tested in the simulator of iPhone 6, iPhone 6+ and iPad Air and worked as expected.

Darth Philou
  • 134
  • 1
  • 9
  • Your solution works perfectly. Thanks a lot. Note that I had to modify the default separateSecondaryViewControllerFromPrimaryViewController and collapseSecondaryViewController methods as well to obtain the behaviour I needed. – phamel Sep 05 '15 at 16:47
  • 2
    @phamel can you share the code modifications you ended up having to make in this case? – SAHM Apr 20 '16 at 12:26
  • The split controller does all this stuff for you. Just subclass the tab controller and override show and forward it on to the child nav controller. – malhal Jan 09 '19 at 12:53
3

Unfortunately, the selected answer didn't work for me. However, I did eventually manage to solve the problem:

  1. Subclass UISplitViewController and set the new class in Interface Builder.
  2. Make the new class conform to UISplitViewControllerDelegate:

    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        self.delegate = self
    }
    
  3. Implement these two methods:

    func splitViewController(_ splitViewController: UISplitViewController,
                             collapseSecondary secondaryViewController:UIViewController,
                             onto primaryViewController:UIViewController) -> Bool {
        return true
    }
    
    func splitViewController(_ splitViewController: UISplitViewController,
                             showDetail vc: UIViewController,
                             sender: Any?) -> Bool {
    
        if splitViewController.isCollapsed {
            guard let tabBarController = splitViewController.viewControllers.first as? UITabBarController else { return false }
            guard let selectedNavigationViewController = tabBarController.selectedViewController as? UINavigationController else { return false }
    
            // Push view controller
            var detailViewController = vc
            if let navController = vc as? UINavigationController, let topViewController = navController.topViewController {
                detailViewController = topViewController
            }
            selectedNavigationViewController.pushViewController(detailViewController, animated: true)
            return true
        }
        return false
    }
    
nikolovski
  • 4,049
  • 1
  • 31
  • 37
0

The docs state when the split controller is collapsed, it handles showDetail by calling show on the master view controller, which in your case is a tab controller. You need to forward that on to the child nav controller as follows:

  1. Make a tab controller subclass.
  2. In the storyboard set the tab controller to use the new subclass.
  3. Add this method to the subclass:
- (void)showViewController:(UIViewController *)vc sender:(id)sender{
    [self.viewControllers.firstObject showViewController:vc sender:sender];
}

This forwards it on to the nav controller in the first tab.

malhal
  • 26,330
  • 7
  • 115
  • 133
  • I'm trying that, but I get an EXC_BAD_ACCESS because it seems like the call to show is recursive. Can you show some code? – Sebastien Apr 19 '19 at 04:16
  • I would guess you are accidentally calling super showViewController in your tab controller subclass. There isn't really any code to show but I'll add some more steps. – malhal Apr 21 '19 at 13:40