31

iOS - Split View Controller - How do I get a pointer (reference) to the Detail View Controller (the bigger right one) from inside the Master View Controller (the smaller left one)?

My gut tell me the Main Split View Controller should have a reference to Detail View Controller and to my own Master View Controller, but I can't figure out how to get it.

Any thoughts?

unom
  • 11,438
  • 4
  • 34
  • 54

3 Answers3

70

Split view controllers do have references to their master and detail view controllers, via the viewControllers property.

In iOS 7.x and below, the viewControllers array should have exactly two view controller objects in it. The first object is the master view controller and the second object is the detail view controller.

In iOS 8.0 and above, the viewControllers array has at least one view controller object in it – the master (or "primary") view controller. If a second view controller object is in the array, then it is the detail (or "secondary") view controller. When the split view controller is collapsed, only the master view controller is in this array, and when expanded it will contain both the master and the detail view controllers.

You can use the splitViewController property of all view controllers to get your split view controller, and from there use the viewControllers property to access either your master or detail view controllers, like so:

Swift:

let masterVC = self.splitViewController?.viewControllers.first
let detailVC = (self.splitViewController?.viewControllers.count > 1) ? self.splitViewController?.viewControllers[1] : nil

Objective-C:

UIViewController *masterVC = [self.splitViewController.viewControllers firstObject];
UIViewController *detailVC;
if (self.splitViewController.viewControllers.count > 1) {
    detailVC = self.splitViewController.viewControllers[1];
}

The splitViewController property works by walking up the view controller hierarchy and trying to find any split view controller that the calling view controller is in. If the view controller is not in a split view controller, then the property is nil. It works the same as the navigationController and tabBarController view controller properties.

You can make the master and detail view controllers easier to access using an extension in Swift (or a category in Objective-C) on UISplitViewController, like so (replacing all the xx_'s with your own prefix if you're using Objective-C):

Swift:

extension UISplitViewController {
    var primaryViewController: UIViewController? {
        return self.viewControllers.first
    }

    var secondaryViewController: UIViewController? {
        return self.viewControllers.count > 1 ? self.viewControllers[1] : nil
    }
}

Objective-C:

// UISplitViewController+ChildViewControllerAccess.h
@interface UISplitViewController (ChildViewControllerAccess)

@property (nonatomic, readonly) UIViewController *xx_primaryViewController;
@property (nonatomic, readonly) UIViewController *xx_secondaryViewController;

@end

// UISplitViewController+ChildViewControllerAccess.m
@implementation UISplitViewController (ChildViewControllerAccess)

- (UIViewController *)xx_primaryViewController
{
    return self.viewControllers.firstObject;
}

- (UIViewController *)xx_secondaryViewController
{
    return self.viewControllers.count > 1 ? self.viewControllers[1] : nil;
}

@end

You can then make use of these properties like so:

Swift:

func someFunctionInSomeViewControllerClass {
    // Get the primary and secondary view controllers if
    // this view controller is in a split view controller.
    // These will be nil if this view controller is not a
    // descendant of a split view controller.
    var primaryVC = self.splitViewController?.primaryViewController
    var secondaryVC = self.splitViewController?.secondaryViewController

    // Do something with them
    primaryVC?.title = "This is the primary VC"
    secondaryVC?.title = "This is the secondary VC"
}

Objective-C:

#import "UISplitViewController+ChildViewControllerAccess.h"

[...]

- (void)someMethodInSomeViewControllerClass
{
    // Get the primary and secondary view controllers if
    // this view controller is in a split view controller.
    // These will be nil if this view controller is not a
    // descendant of a split view controller.
    UIViewController *primaryVC = self.splitViewController.xx_primaryViewController;
    UIViewController *secondaryVC = self.splitViewController.xx_secondaryViewController;

    // Do something with them
    primaryVC.title = @"This is the primary VC";
    secondaryVC.title = @"This is the secondary VC";
}
TylerP
  • 9,600
  • 4
  • 39
  • 43
  • 2
    Wow... didn't know all View Controllers have a splitViewController property for this exact purpose. Great! Thank you! – unom Aug 18 '14 at 08:42
  • 6
    `viewControllers` property _may_ contain only one view controller, "..When the split view interface is expanded, this property contains two view controllers; **when it is collapsed, this property contains only one view controller**. The first view controller in the array is always the primary (or master) view controller. If a second view controller is present, that view controller is the secondary (or detail) view controller..." – adib May 09 '15 at 07:11
  • @adib Thanks, adib! I had no idea. I've updated the answer to reflect how the `viewControllers` array works in iOS 8. I also added some code for a category to make accessing these view controllers (somewhat) easier. – TylerP May 09 '15 at 09:47
  • 5
    How do i get to the DetailsViewController when its collapsed? – ilan Jul 26 '16 at 13:45
  • If you apply the methods given by MrSaturn to the split view in Xcode's Master-Detail template project, `primaryVC` will be a `UINavigationController`. Well, OK, `UINavigationController` inherits from `UIViewController`. To get the view controller you probably want, downcast `primaryVC` to a `UINavigationController` and get its `topViewController`. – Jerry Krinock Dec 24 '16 at 01:48
  • Can anybody give a working example of the extension for the Master-Detail project. I've tried the following: `import UIKit extension UISplitViewController { var ggida_primaryViewController: UIViewController? { get { let navController = self.viewControllers.first! as! UINavigationController return navController.topViewController! as UIViewController } } }` – Jack Robson Jan 14 '17 at 13:23
  • I get the following error: Could not cast value of type 'UINavigationController' (0x10abae008) to 'GoGetIt_Driver_App.MasterViewController' (0x10826c350). – Jack Robson Jan 14 '17 at 13:26
  • When `splitViewController` is going to uncollapse, and its `masterViewController` is an instance of UINavigationViewController it uncollapses the detail from currently displayed top controller of the navigationController stack. I would prefer `splitViewController` to always uncollapse the detailViewController which is segued to in a story board directly from `splitViewController`. How can I make it? – Voyteck Feb 16 '17 at 08:21
2

If you are using iOS 14+, you can get a specific viewController from splitViewController using viewController(for:) method:

let detailViewController = splitViewController.viewController(for: .secondary) as? DetailViewController
pableiros
  • 14,932
  • 12
  • 99
  • 105
1

Create a property in your UISplitViewController subclass:

var _detailViewController: UIViewController? {
    get {
        if viewControllers.count > 1 {
            return viewControllers[1] as? UIViewController
        }
        return nil
    }
}

According to Apple's documentation, this should sometimes return nil, but in my experience, it always returns the detail view controller, regardless of state.

Also, do not call this property "detailViewController" instead of "_detailViewController" - Apple is apparently already using that name under the hood, and it will mess with your UI.

UISplitViewController is really hokey and needs a lot of cleanup and corrected documentation...

jeremywhuff
  • 2,911
  • 3
  • 29
  • 33