UISplitViewController in portrait on iPhone shows detail VC instead of master
Asked Answered
U

15

187

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.

Unkempt answered 16/9, 2014 at 18:4 Comment(0)
O
246

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.

Obel answered 17/9, 2014 at 17:12 Comment(19)
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. :)Unkempt
This is a great walkthrough of the implementation details of Apple's new Master-Detail Template, and the changes in iOS 8. Thanks!!Tanagra
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 helpBohaty
@ChrisSlade is right. Though I guess it depends on if you always want your detail VC to be displayed in portrait.Obel
@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.Tanagra
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).Bucentaur
@nickv2002 I'm guessing @Joey made his UISplitViewController subclass it's own delegate. Did you do that?Obel
@Tanagra 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.Obel
@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.Tanagra
@joey's comment works with setting self.delegate = self; in the viewdidload! And adding <UISplitViewControllerDelegate> in the .h Thankyou!Trowbridge
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?Aguila
@TimDean are you using a storyboard for your split view controller, or are you creating it programmatically in your app delegate?Obel
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.Aguila
It not call to that delegate when use iPad in portrait. iPhone is okTerbium
If you can't get the views to separate on the iPhone 6 plus make sure you have a landscape launch image.Snowfall
@TimDean have you found why it is never called, other delegates are calling right but that is collapseSecondaryViewController is not called ? :(Entrails
@Divjyot It is not called on iPad or iPhone 6 Plus, see also here: #29768114Sixtasixteen
@Koen I am build for Target iPad only and still not called !Entrails
@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.Parturifacient
B
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
  }

}
Brownley answered 1/5, 2015 at 0:10 Comment(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 = truePaleobotany
Now in Swift whatever it's func splitViewController(_ splitViewController: UISplitViewController, collapseSecondary secondaryViewController: UIViewController, onto primaryViewController: UIViewController) -> Bool {Brendabrendan
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.1Bikales
O
24

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.

Octahedral answered 16/12, 2015 at 15:33 Comment(1)
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. :)Mckenna
T
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
Twilatwilight answered 17/12, 2014 at 8:47 Comment(0)
F
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
}
Friesland answered 3/7, 2016 at 3:48 Comment(4)
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. ThanksParturifacient
Many methods are slightly renamed.Heptachord
Tony's answer is the Swift 3 syntax to @Octahedral 's answer (which is written for previous Swift versions)Apiculate
for swift 3, don't forget to put self.delegate = selfon viewDidLoad method.Wenz
O
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
}
Oglesby answered 30/6, 2017 at 15:28 Comment(0)
B
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.

Bernardabernardi answered 17/2, 2015 at 3:35 Comment(2)
good trick. My app is only in portrait mode and I can do this.Stradivari
This is true except in Landscape orientation you will see empty right part of the view possibly gray filled.Itinerary
D
4

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.

Decrement answered 17/4, 2016 at 22:5 Comment(2)
The delegate method never gets called!Gove
it's not called on the iPad and iPhone 6/7/8 Plus. Is that your problem? Have a look at: #29768114Decrement
P
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

Piffle answered 28/7, 2017 at 17:9 Comment(1)
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) -> BoolLenard
E
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;
}
Enjoyable answered 24/12, 2016 at 22:58 Comment(0)
W
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
    }
}
Wanwand answered 30/12, 2017 at 18:28 Comment(0)
W
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
    }

}
Woad answered 9/11, 2018 at 9:15 Comment(0)
B
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;
    }
}
Bernini answered 30/10, 2017 at 16:20 Comment(0)
O
0

Just set the preferredDisplayMode property of UISplitViewController to .allVisible

class MySplitViewController: UISplitViewController {

    override func viewDidLoad() {
        super.viewDidLoad()

        self.preferredDisplayMode = .allVisible
    }

}
Octennial answered 15/6, 2019 at 13:6 Comment(0)
L
0

From iOS 14 and above you can use this method from UISplitViewConrtollerDelegate.

@available(iOS 14.0, *)
func splitViewController(_ svc: UISplitViewController, topColumnForCollapsingToProposedTopColumn proposedTopColumn: UISplitViewController.Column) -> UISplitViewController.Column {
        return .primary
}
Libby answered 19/4 at 12:53 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.