UISplitViewController in portrait on iPhone shows detail VC instead of master

IosInterface BuilderIos8UisplitviewcontrollerMaster Detail

Ios Problem Overview


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.

Ios Solutions


Solution 1 - Ios

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
  1. If your view is not correct
  • show the master view controller

Clear as mud.

Solution 2 - Ios

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
  }

}

Solution 3 - Ios

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.

Solution 4 - Ios

   #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

Solution 5 - Ios

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
}

Solution 6 - Ios

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
}

Solution 7 - Ios

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.

Solution 8 - Ios

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.

Solution 9 - Ios

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

Solution 10 - Ios

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

Solution 11 - Ios

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

Solution 12 - Ios

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

Solution 13 - Ios

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

Solution 14 - Ios

Just set the preferredDisplayMode property of UISplitViewController to .allVisible

class MySplitViewController: UISplitViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        
        self.preferredDisplayMode = .allVisible
    }
    
}

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionJordan HView Question on Stackoverflow
Solution 1 - IosMark SView Answer on Stackoverflow
Solution 2 - IosClifton LabrumView Answer on Stackoverflow
Solution 3 - IosNiñoScriptView Answer on Stackoverflow
Solution 4 - IosGankView Answer on Stackoverflow
Solution 5 - IosTonyView Answer on Stackoverflow
Solution 6 - IosoliView Answer on Stackoverflow
Solution 7 - IosHao-Cher HongView Answer on Stackoverflow
Solution 8 - Iosuser1199624View Answer on Stackoverflow
Solution 9 - IosBartosz KunatView Answer on Stackoverflow
Solution 10 - IosBorys ShcherbynaView Answer on Stackoverflow
Solution 11 - IosVishal ChaudhryView Answer on Stackoverflow
Solution 12 - IosSaeed IrView Answer on Stackoverflow
Solution 13 - IosMark MoeykensView Answer on Stackoverflow
Solution 14 - IosArash EtemadView Answer on Stackoverflow