UISplitViewController collapse code for iOS 14 and later

Published by malhal on

A new collapse delegate was added in Xcode 12 and iOS 14. Unfortunately the Master-Detail template was removed so there has never been a good example of how to use it to maintain the behavior of the previous template which was to show the master if no detail is showing. The only example I could find is in SoupChef but it simply returns .primary regardless. To come up with a solid example in Swift, I adapted the syntax in the old master-detail template and came up with this:

func splitViewController(_ svc: UISplitViewController, topColumnForCollapsingToProposedTopColumn proposedTopColumn: UISplitViewController.Column) -> UISplitViewController.Column {
    let secondaryViewController = svc.viewController(for: .secondary)
    guard let secondaryAsNavController = secondaryViewController as? UINavigationController else { return .primary }
    guard let topAsDetailController = secondaryAsNavController.topViewController as? DetailViewController else { return .primary }
    if topAsDetailController.detailItem == nil {
        return .primary
    }
    return .secondary
}

That viewController(for:) method is also new in iOS 14. It even can return the detail when the split view controller is collapsed and the master is showing in the navigation controller. This was impossible before without hanging on to the detail yourself somewhere. Also, the new delegate is only called when the split view controller type is set to double (or triple) column in the storyboard.

For reference, here is the code from the last master-detail template that Xcode had:

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

And here is the Obj-C code that it was ported from (which I always found rather ugly):

- (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;
    }
}
Categories: SwiftUIKit