How I Now Deal with Collapsible Split View Controllers on the iPhone 6

So UISplitViewController is the new default for iOS apps. Most iPhones have compact-sized size classes. iPads have regular sized ones. The iPhone 6(s) Plus mixes both. Only horizontally regular-sized environments show both the master and the detail scene (or primary and secondary view controller).

When you rotate from portrait to landscape, the “Plus” phones split the screen’s contents like iPads do. That’s because they are not of compact but of regular height. When rotating, this translates to regular width.

So depending on the device orientation, the screen will have one compact and one regular-sized dimension.

Now UIKit provides two methods to deal with this switch, called collapsing and expanding:

- (BOOL)splitViewController:(UISplitViewController *)splitViewController 
collapseSecondaryViewController:(UIViewController *)secondaryViewController 
      ontoPrimaryViewController:(UIViewController *)primaryViewController;

- (UIViewController *)splitViewController:(UISplitViewController *)splitViewController 
separateSecondaryViewControllerFromPrimaryViewController:(UIViewController *)primaryViewController;

Handling collapsing is pretty easy. You return YES or NO. The three parameters give you plenty of context to make an informed decision.

Note that I let the master view controller implement UISplitViewControllerDelegate to provide functionality.

For Calendar Paste 3, it looks like this:

- (BOOL)splitViewController:(UISplitViewController *)splitViewController collapseSecondaryViewController:(UIViewController *)secondaryViewController ontoPrimaryViewController:(UIViewController *)primaryViewController
    if ([secondaryViewController.restorationIdentifier isEqualToString:kViewControllerNoSelection])
        // Always collapse empty detail
        return YES;
    // Don't collapse paste detail
    return NO;

There’s a “No selection” detail scene when rotating to landscape. It’s so boring that it should collapse when rotating back to portrait so the table view is visible instead. When there’s meaningful content to the user displayed as detail, this content should become visible when rotating into portrait instead of the table view. Easy.

Calendar Paste screen shot
Empty detail scene for calendar paste

What should rotating back into landscape do?

The separation-callback doesn’t tell what the secondary view controller is going to be.

How do you find out which detail view controller is currently visible?

My master view controller, which is the delegate, does this:

- (UIViewController *)splitViewController:(UISplitViewController *)splitViewController separateSecondaryViewControllerFromPrimaryViewController:(UIViewController *)primaryViewController
    if ([self isDisplayingPasteDetailSceneWithPrimaryViewController:primaryViewController])
        // Keep the detail scene
        return nil;

    // Replace the detail scene with the "empty" scene
    return [self emptySelectionNavigationController];;

Apart from the hideous method name isDisplayingPasteDetailSceneWithPrimaryViewController:, it’s still not too complicated to understand what the logic is.

What does it look like, you ask?

- (BOOL)isDisplayingPasteDetailSceneWithPrimaryViewController:(UIViewController *)primaryViewController
    if ([primaryViewController isKindOfClass:[UINavigationController class]]) {
        UINavigationController *navController = (UINavigationController *)primaryViewController;
        if (navController.viewControllers.count > 1) {
            UINavigationController *detailNavController = (UINavigationController *)navController.viewControllers[1];
            id visibleDetailViewController = detailNavController.viewControllers.firstObject;
            if ([visibleDetailViewController isKindOfClass:[ShiftAssignmentViewController class]]) {
                return true;

    return false;

It’s a nightmare!

Here, I strongly couple knowledge about implementation details of UISplitViewController and UINavigationController into the master view controller.

Fun facts:

After all it seems UISplitViewController does weird things to the master’s UINavigationController to achieve its magic.

This makes it reasonable for Apple to not provide a secondaryViewController parameter in -splitViewController:separateSecondaryViewControllerFromPrimaryViewController:since the split view controller doesn’t know after the detail was pushed onto the view controller stack of the master.

In this weird environment that’s called iPhone 6(s) Plus, where collapsing and separation of UISplitViewControllers is possible, only the master’s UINavigationController knows which soon-to-become detail is on top. Reaching up from a view controller is bad. That’s a very strong argument against making any view controller the UISplitViewControllerDelegate.

Ah, the joys of legacy code.

The delegate should be something else entirely, it seems. An object which encapsulates so-called knowledge about the way UIKit does its thing. (I should call this “assumptions” instead.)

I cannot come up with something more clever. So here it is:

import UIKit

let kViewControllerNoSelection = "NoSelectionNC"

class CollapsibleSplitViewControllerDelegate: NSObject {

    let storyboard: UIStoryboard

    init(storyboard: UIStoryboard) {
        self.storyboard = storyboard

extension CollapsibleSplitViewControllerDelegate: UISplitViewControllerDelegate {

    func splitViewController(splitViewController: UISplitViewController, collapseSecondaryViewController secondaryViewController: UIViewController, ontoPrimaryViewController primaryViewController: UIViewController) -> Bool {
        if let viewControllerIdentifier = secondaryViewController.restorationIdentifier where viewControllerIdentifier == kViewControllerNoSelection {
            // Always collapse empty detail
            return true
        // Leave detail on top
        return false

    func splitViewController(splitViewController: UISplitViewController, separateSecondaryViewControllerFromPrimaryViewController primaryViewController: UIViewController) -> UIViewController? {
        if isDisplayingPasteDetailScene(primaryViewController: primaryViewController) {
            // Keep the detail scene (.None indicates the framework 
            // will figure out what to do)
            return .None
        return emptySelectionNavigationController()

    func isDisplayingPasteDetailScene(primaryViewController viewController: UIViewController) -> Bool {
        guard let navController = viewController as? UINavigationController, detailNavController = navController.topViewController as? UINavigationController else {
            return false
        guard detailNavController.topViewController is ShiftAssignmentViewController else {
            return false
        return true

    func emptySelectionNavigationController() -> UIViewController {
        return storyboard.instantiateViewControllerWithIdentifier(kViewControllerNoSelection)

It fixes my problems, but it doesn’t make me like UIKit more. The new stuff helps develop apps quickly and all, but it’s a pain to do without producing bad code, apparently.

Browse the blog archive