183

I am using a Universal Storyboard in Xcode 6, targeting iOS 7 and above. I've implemented a UISplitViewController which is now natively supported on iPhone running iOS 8, and Xcode will automatically backport it for iOS 7. It's working really well, except when you launch the app on iPhone in portrait running iOS 8, the split view's detail view controller is displayed when I expected to first see the master view controller. I believed this was a bug with iOS 8 because when you run the app on iOS 7, it correctly shows the master view controller. But iOS 8 is now GM and this is still occurring. How can I set it up so that when the split view controller is going to be collapsed (only one view controller displayed on screen), when the split view controller is displayed it shows the master view controller not the detail?

I've created this split view controller in Interface Builder. The split view controller is the first view controller within a tab bar controller. Both the master and the detail VCs are navigation controllers with table view controllers embedded inside.

Jordan H
  • 45,794
  • 29
  • 162
  • 306

14 Answers14

244

Oh man, this was causing me a headache for a few days and could not figure out how to do this. The worst part was that creating a new Xcode iOS project with the master-detail template worked just fine. Fortunately, in the end, that little fact was how I found the solution.

There are some posts I've found that suggest that the solution is to implement the new primaryViewControllerForCollapsingSplitViewController: method on UISplitViewControllerDelegate. I tried that to no avail. What Apple does in the master-detail template that seems to work is implement the new (take a deep breath to say all of this one) splitViewController:collapseSecondaryViewController:ontoPrimaryViewController: delegate method (again on UISplitViewControllerDelegate). According to the docs, this method:

Asks the delegate to adjust the primary view controller and to incorporate the secondary view controller into the collapsed interface.

Make sure to read up on the discussion part of that method for more specific details.

The way that Apple handles this 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 {

        return NO;

    }
}

This implementation basically does the following:

  1. If secondaryViewController is what we're expecting (a UINavigationController), and it's showing what we're expecting (a DetailViewController -- your view controller), but has no model (detailItem), then "Return YES to indicate that we have handled the collapse by doing nothing; the secondary controller will be discarded."
  2. Otherwise, return "NO to let the split view controller try and incorporate the secondary view controller’s content into the collapsed interface"

The results are the following for the iPhone in portrait (either starting in portrait or rotating to portrait -- or more accurately compact size class):

  1. If your view is correct
    • and has a model, show the detail view controller
    • but has no model, show the master view controller
  2. If your view is not correct
    • show the master view controller

Clear as mud.

Mark S
  • 2,479
  • 1
  • 12
  • 7
  • 8
    Fantastic answer! I simply subclassed `UISplitViewController` and always return `YES` from that method, then just changed the split view class in Storyboard, as I always want to show the master on iPhone in portrait. :) – Jordan H Sep 17 '14 at 22:41
  • This is a great walkthrough of the implementation details of Apple's new Master-Detail Template, and the changes in iOS 8. Thanks!! – oflannabhra Sep 23 '14 at 13:30
  • 2
    I want my master view controller to be hidden if the "iPhone" is in "Portrait" mode because i have a default detail view controller setup. How can I do that. My master and detail both are of the type VC. Specifically my detail is MMDrawerController. Please help – Harshit Gupta Sep 25 '14 at 20:13
  • @ChrisSlade is right. Though I guess it depends on if you _always_ want your detail VC to be displayed in portrait. – Mark S Sep 26 '14 at 17:50
  • @MarkS Just a follow-up, the default behavior here is to collapse a `UINavigationController` _into_ another `UINavigationController`. This is a bit crazy in my mind, and as soon as we get past the simple behaviour Apple's MasterDetailTemplate.app handles, presents a _lot_ of problems. – oflannabhra Nov 07 '14 at 21:16
  • 3
    I tried Joey's suggestion of subclassing `UISplitViewController` but found that that didn't work:`splitViewController:collapseSecondaryViewController:ontoPrimaryViewController:` was never called. Instead I copied Apple's template and put it in the AppDelagate. This necessitated a few changes to creating the UISplitViewController under `application didFinishLaunchingWithOptions:` as well (where I also copied Apple's template). – Nick Nov 09 '14 at 07:51
  • @nickv2002 I'm guessing @Joey made his `UISplitViewController` subclass it's own delegate. Did you do that? – Mark S Nov 10 '14 at 17:39
  • @oflannabhra First of all, doesn't have to be 2 nav controllers. They can just be view controllers. Second of all, it's not any kind of real collapse or merge of any kind. It's more hiding one of the view controllers. I'm not sure that I necessarily agree with how Apple implemented this idea, but there's not much we can do about it. Unless you want to implement your own `UISplitViewController`. – Mark S Nov 10 '14 at 19:06
  • @MarkS sorry, when I said 'default' I meant Apple's Master Detail Template project (where your `collapse:` method is from) by default contains two navigation controllers and it inserts the secondary _into_ the primary. Obviously, you don't _have_ to use navigation controllers, but Apple does in that project. – oflannabhra Nov 10 '14 at 19:26
  • 8
    @joey's comment works with setting self.delegate = self; in the viewdidload! And adding in the .h Thankyou! – fellowworldcitizen Nov 17 '14 at 10:27
  • 2
    This seems like the right answer for me, as I am having the exact same problem. However, for some reason my `splitViewController:collapseSecondaryViewController:ontoPrimaryViewController:` never gets called. It appears that the delegate is being properly set my app delegate's `applicationDidFinishLaunchingWithOptions:` method. Has anyone else seen this problem and NOT had this solution work? – Tim Dean Jan 12 '15 at 01:24
  • @TimDean are you using a storyboard for your split view controller, or are you creating it programmatically in your app delegate? – Mark S Jan 12 '15 at 19:01
  • I am using a storyboard. The code was created in the Apple template, and it looks pretty much as you describe here. Except that it doesn't ever seem to be called. – Tim Dean Jan 12 '15 at 19:18
  • It not call to that delegate when use iPad in portrait. iPhone is ok – huync May 25 '15 at 08:48
  • If you can't get the views to separate on the iPhone 6 plus make sure you have a landscape launch image. – Steve Moser Jun 27 '15 at 01:09
  • Super helpful! Thanks! – Matt Long Oct 07 '15 at 05:04
  • 1
    @TimDean have you found why it is never called, other delegates are calling right but that is collapseSecondaryViewController is not called ? :( – djay Jul 14 '16 at 14:06
  • 1
    @Divjyot It is not called on iPad or iPhone 6 Plus, see also here: http://stackoverflow.com/questions/29767614/why-splitviewcontrollercollapsesecondaryviewcontrollerontoprimaryviewcontrolle – koen Jul 16 '16 at 20:37
  • @Koen I am build for Target iPad only and still not called ! – djay Jul 18 '16 at 06:47
  • @Mark S When you drag and drop a Split View Controller from the Object library in Main.storyboard, you have the split view controller at the front and then a fork with Navigation Controller and RootViewController at the top and a ViewController at the bottom. Which is the primary ViewController and which is the secondary ViewController. I am confused. Thank you. – bibscy Dec 13 '16 at 13:28
62

Here is the accepted answer in Swift. Just create this subclass and assign it to your splitViewController in your storyboard.

//GlobalSplitViewController.swift

import UIKit

class GlobalSplitViewController: UISplitViewController, UISplitViewControllerDelegate {

  override func viewDidLoad() {
    super.viewDidLoad()

    self.delegate = self
  }

  func splitViewController(splitViewController: UISplitViewController, collapseSecondaryViewController secondaryViewController: UIViewController!, ontoPrimaryViewController primaryViewController: UIViewController!) -> Bool{
    return true
  }

}
Clifton Labrum
  • 9,827
  • 7
  • 45
  • 86
  • 3
    Great, this helps a lot. But a new problem arose. The backbutton that takes me to the master now disappear (never shows). How do I get It back? EDIT: Never mind, figured myself :-). For other users: add this in the detailView: self.navigationItem.leftBarButtonItem = self.splitViewController?.displayModeButtonItem() self.navigationItem.leftItemsSupplementBackButton = true – Tom Tallak Solbu Mar 29 '16 at 13:51
  • 3
    Now in Swift whatever it's `func splitViewController(_ splitViewController: UISplitViewController, collapseSecondary secondaryViewController: UIViewController, onto primaryViewController: UIViewController) -> Bool {` – Dan Rosenstark Apr 23 '18 at 23:26
  • 3
    It seems like that delegate method is only being called when the class size is compact. It is being called on iPhone, but not on iPad portrait, which means it doesn't solve the problem, since iPad portrait is also in collapsed mode. Tested with iOS 12.1 – Daniel Mar 25 '19 at 09:57
21

Swift version of Mark S' correct answer

As provided by Apple's Master-Detail template.

func splitViewController(splitViewController: UISplitViewController, collapseSecondaryViewController secondaryViewController:UIViewController, ontoPrimaryViewController primaryViewController:UIViewController) -> Bool {
    guard let secondaryAsNavController = secondaryViewController as? UINavigationController else { return false }
    guard let topAsDetailController = secondaryAsNavController.topViewController as? DetailViewController else { return false }
    if topAsDetailController.detailItem == nil {
        // Return true to indicate that we have handled the collapse by doing nothing; the secondary controller will be discarded.
        return true
    }
    return false
}

Clarification

(What Mark S said was slightly confusing)

This delegate method is called splitViewController: collapseSecondaryViewController: ontoPrimaryViewController:, because that's what it does. When changing to a more compact width size (for example when rotating the phone from landscape to portrait), it needs to collapse the split view controller into only one of them.

This function returns a boolean to decide if it should collapse the Detail and show the Master or not.

So in our case, we'll decided based on if there was a detail selected or not. How do we know if our detail is selected? If we follow Apple's Master-Detail template, the detail view controller should have an optional variable having the detail info, so if it's nil (.None), there's nothing selected yet and we should show the Master so the user can select something.

That's it.

Community
  • 1
  • 1
NiñoScript
  • 4,279
  • 2
  • 25
  • 31
  • Just to clarify why I rolled back from @sschale's edit. That code is a quote from `Apple's Master-Detail template`, it's not intended to be great or concise, just factual. :) – NiñoScript Jun 01 '16 at 21:07
9
   #import <UIKit/UIKit.h>

    @interface SplitProductView : UISplitViewController<UISplitViewControllerDelegate>




    @end

.m:

#import "SplitProductView.h"
#import "PriceDetailTableView.h"

@interface SplitProductView ()

@end

@implementation SplitProductView

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    self.delegate = self;
}

- (void)didReceiveMemoryWarning {
    [super didReceiveMemoryWarning];
    // Dispose of any resources that can be recreated.
}

/*
#pragma mark - Navigation

// In a storyboard-based application, you will often want to do a little preparation before navigation
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
    // Get the new view controller using [segue destinationViewController].
    // Pass the selected object to the new view controller.
}
*/
- (BOOL)splitViewController:(UISplitViewController *)splitViewController
collapseSecondaryViewController:(UIViewController *)secondaryViewController
  ontoPrimaryViewController:(UIViewController *)primaryViewController {

    if ([secondaryViewController isKindOfClass:[UINavigationController class]]
        && [[(UINavigationController *)secondaryViewController topViewController] isKindOfClass:[PriceDetailTableView class]]

        //&& ([(PriceDetailTableView *)[(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 {

        return NO;

    }
}
@end
Gank
  • 5,056
  • 4
  • 43
  • 44
9

My app was written in Swift 2.x and could run well. After converting it into Swift 3.0 (using XCode converter), it starts showing detail first instead of master in portrait mode. The problem is the name of the function splitViewController is not changed to match the new one of UISplitViewControllerDelegate.

After changing that function's name manually, my app now can work correctly:

func splitViewController(_ splitViewController: UISplitViewController, collapseSecondary secondaryViewController:UIViewController, onto primaryViewController:UIViewController) -> Bool {
    guard let secondaryAsNavController = secondaryViewController as? UINavigationController else { return false }
    guard let topAsDetailController = secondaryAsNavController.topViewController as? DetailViewController else { return false }
    if topAsDetailController.game == nil {
        // Return true to indicate that we have handled the collapse by doing nothing; the secondary controller will be discarded.
        return true
    }
    return false
}
Tony
  • 1,421
  • 18
  • 19
  • I am having the same problem as you,but I don't understand your solution. I don't see any change in the code you have posted here. Could you be more specific. Thanks – bibscy Dec 12 '16 at 17:26
  • Many methods are slightly renamed. – Dave Jan 28 '17 at 22:05
  • Tony's answer is the Swift 3 syntax to @NiñoScript 's answer (which is written for previous Swift versions) – Hellojeffy Feb 15 '17 at 14:52
  • 2
    for swift 3, don't forget to put `self.delegate = self`on `viewDidLoad` method. – Fer Jul 11 '17 at 06:48
9

From the documentation, you need to use a delegate to tell the UISplitViewController not to incorporate the detail view into the "collapsed interface" (i.e. the "Portrait mode" in your case). In Swift 4, the delegate method to implement for that has been renamed:

func splitViewController(_ splitViewController: UISplitViewController, collapseSecondary secondaryViewController:UIViewController, onto primaryViewController:UIViewController) -> Bool {
    return true
}
oli
  • 1,548
  • 14
  • 14
7

If you don't have default values to show in detail view controller, you could just simply delete the default segue between the SplitViewController and your detail UIViewController in the story board. This will make it always goes into Master View Controller first.

The side effect of this is that instead of seeing two view in landscape, you will see one view in full size in SplitViewController until Show Detail Segue in master view controller fired.

Hao-Cher Hong
  • 192
  • 1
  • 6
4

For all the people who couldn't find cs193p's friday section:

In Swift 3.1.1 creating a subclass of UISplitViewController and implementing one of its delegate methods worked for me like a charm:

class MainSplitViewController: UISplitViewController, UISplitViewControllerDelegate {

override func viewDidLoad() {
    super.viewDidLoad()
    self.delegate = self
}

func splitViewController(_ splitViewController: UISplitViewController, collapseSecondary secondaryViewController: UIViewController, onto primaryViewController: UIViewController) -> Bool {
    return true
} }

My storyboard

Bartosz Kunat
  • 1,043
  • 15
  • 22
  • As @olito has pointed out, in Swift 4 the syntax for this has changed to: `public func splitViewController(_ splitViewController: UISplitViewController, collapseSecondary secondaryViewController: UIViewController, onto primaryViewController: UIViewController) -> Bool` – Robuske Aug 24 '17 at 21:36
3

In my opinion you should solve this problem more generic. You can subclass the UISplitViewController and implement a protocol in the embedded view controllers.

class MasterShowingSplitViewController: UISplitViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        delegate = self
    }
}

extension MasterShowingSplitViewController: UISplitViewControllerDelegate {
    func splitViewController(splitViewController: UISplitViewController,
                             collapseSecondaryViewController secondaryViewController: UIViewController,
                             ontoPrimaryViewController primaryViewController: UIViewController) -> Bool {
        guard let masterNavigationController = primaryViewController as? UINavigationController,
                  master = masterNavigationController.topViewController as? SplitViewControllerCollapseProtocol else {
            return true
        }
        return master.shouldShowMasterOnCollapse()

    }
}

protocol SplitViewControllerCollapseProtocol {
    func shouldShowMasterOnCollapse() -> Bool
}

Example implementation in UITableViewController:

extension SettingsTableViewController: SplitViewControllerCollapseProtocol {
    func shouldShowMasterOnCollapse() -> Bool {
        return tableView.indexPathForSelectedRow == nil
    }
}

Hope it helps. So you can reuse this class and just need to implement a protocol.

  • The delegate method never gets called! – K_Mohit Aug 30 '17 at 19:35
  • it's not called on the iPad and iPhone 6/7/8 Plus. Is that your problem? Have a look at: https://stackoverflow.com/questions/29767614/why-splitviewcontrollercollapsesecondaryviewcontrollerontoprimaryviewcontrolle –  Nov 27 '17 at 13:11
2

Just remove DetailViewController from SplitView controllers when you need it to start from Master.

UISplitViewController *splitViewController = (UISplitViewController *)[self.storyboard instantiateViewControllerWithIdentifier:@"SETTINGS"];
splitViewController.delegate = self;
[self.navigationController presentViewController:splitViewController animated:YES completion:nil];
if (IPHONE) {
    NSMutableArray * cntlrs = [splitViewController.viewControllers mutableCopy];
    [cntlrs removeLastObject];
    splitViewController.viewControllers = cntlrs;
}
2

This worked for me on iOS-11 and Swift 4:

//Following code in application didFinishLaunching (inside Application Delegate)
guard let splitViewController = window?.rootViewController as? UISplitViewController,
            let masterNavVC = splitViewController.viewControllers.first as? UINavigationController,
            let masterVC = masterNavVC.topViewController as? MasterViewController
else { fatalError() }
splitViewController.delegate = masterVC

//Following code in MasterViewController class
extension MasterViewController:UISplitViewControllerDelegate {
    func splitViewController(_ splitViewController: UISplitViewController, collapseSecondary secondaryViewController: UIViewController, onto primaryViewController: UIViewController) -> Bool {
        return true
    }
}
Vishal Chaudhry
  • 2,624
  • 1
  • 20
  • 8
2

The function is renamed in new versions of Swift, so this code works on Swift 4:

import UIKit

class GlobalSplitViewController: UISplitViewController, UISplitViewControllerDelegate {

    override func viewDidLoad() {
        super.viewDidLoad()
        self.delegate = self
    }

    func splitViewController(_ splitViewController: UISplitViewController, collapseSecondary secondaryViewController: UIViewController, onto primaryViewController: UIViewController) -> Bool {
        return true
    }

}
Saeed Ir
  • 1,021
  • 10
  • 15
0

Xamarin / C# Solution

public partial class MainSplitViewController : UISplitViewController
{
    public MainSplitViewController(IntPtr handle) : base(handle)
    {
    }

    public override void ViewDidLoad()
    {
        base.ViewDidLoad();

        Delegate = new MainSplitViewControllerDelegate();
    }
}

public class MainSplitViewControllerDelegate : UISplitViewControllerDelegate
{
    public override bool CollapseSecondViewController(UISplitViewController splitViewController, UIViewController secondaryViewController, UIViewController primaryViewController)
    {
        return true;
    }
}
Community
  • 1
  • 1
Mark Moeykens
  • 11,251
  • 4
  • 57
  • 56
0

Just set the preferredDisplayMode property of UISplitViewController to .allVisible

class MySplitViewController: UISplitViewController {

    override func viewDidLoad() {
        super.viewDidLoad()

        self.preferredDisplayMode = .allVisible
    }

}
Arash Etemad
  • 1,585
  • 1
  • 11
  • 25