Split View Controller Display Modes

The split view controller is a complicated controller to master with many configuration properties and delegate methods with long complicated names. In this post I take a look at a minimal Xcode project to create my preferred setup for an adaptable master-detail layout that keeps both view controllers visible when screen size allows.

Last updated: Jun 12, 2020

Split Views - A Recap

I am going to assume you are familiar with the basic setup of a split view controller. It is a container for a master-detail style interface. Actions or choices made by the user in the primary (master) view controller change what the secondary (detail) view controller displays.

In the example below I have a master view controller (M) with a single button though it might typically be a table view. The button action updates a timestamp in the detail view controller (D) displayed side by side with the master view.

Master-detail split view

The display mode button allows the user to switch between having both views visible and having the primary view hidden as below:

Primary hidden

The split view controller started as an iPad only controller but in iOS 8 gained adaptability to make it available on all iOS devices. When presented in a compact width the split view controller collapses the two views into the familiar navigation view hierarchy. By default, it shows the detail view controller when collapsed.

Collapsed iPhone showing detail view

With horizontally regular size classes such as the iPad you can choose between display modes to show both view controllers side-by-side or have the master view controller hidden or overlaying the detail view controller. These modes also collapse down gracefully when the user switches to a slide over or split screen mode.

In this post I am going to concentrate on the display mode that keeps both views visible when possible. I much prefer this to the default which hides the master view but it is easy to adapt to the other modes if required.

Preferred Display Mode

The preferredDisplayMode property is what controls the arrangement of the split view interface when it is not collapsed. It has four possible values (Objective-C values shown in parentheses):

  • primaryHidden (UISplitViewControllerDisplayModePrimaryHidden) As the name suggests this mode hides the primary view controller.

  • allVisible (UISplitViewControllerDisplayModeAllVisible) Display both the primary and secondary view controllers side-by-side.

  • primaryOverlay (UISplitViewControllerDisplayModePrimaryOverlay) Show the primary view controller overlaying and partly hiding the secondary view controller.

  • automatic (UISplitViewControllerDisplayModeAutomatic) This is the default where the split view controller chooses the display mode based on the horizontal space. It is also what you get if you use the Xcode master-detail template.

For the iPad the automatic mode switches between allVisible in landscape and primaryOverlay in portrait. For the larger iPhone Plus and Max devices it uses allVisible in landscape. For other devices and orientations the split view controller collapses the interface.

Basic Setup for All Visible Split View

I don’t like the Xcode master-detail template for creating a split view controller. It uses the app delegate as the split view controller delegate and I much prefer to use the primary or master view controller (or a parent root view controller). I also find the allVisible display mode to be a better starting point most of the time. So let’s look at the steps to create a basic setup that prefers to show both view controllers side-by-side when possible.

Creating the Split View Controller

You can easily create the view controllers in code but for brevity I will show it in a Storyboard:

Split View Controller Storyboard

The master and detail view controllers are both embedded in navigation controllers. To keep it simple I have a single button in the master view controller that fires a show detail segue to the detail navigation controller to set the timestamp.

Configuring the Split View Controller

Even if you are creating the split view controller in a Storyboard you still need some code for configuration you cannot do in Interface Builder.

We need to set the split view controller delegate, change the preferred display mode to allVisible and add the display mode button to the secondary/detail view controller.

I do all of those steps in the application:didFinishLaunchingWithOptions: method of the application delegate. First we need to find our view controllers after they have been loaded from the storyboard. The split view controller should be the root view controller of the window:

guard let splitViewController = self.window?.rootViewController 
  as? UISplitViewController else {
  fatalError("Missing SplitViewController")
}

The master view controller is the first view controller in the split view, but it’s embedded in a navigation controller:

guard let masterNavController = splitViewController.viewControllers.first 
  as? UINavigationController,
  let masterViewController = masterNavController.topViewController 
  as? MasterViewController else {
  fatalError("Missing MasterViewController")
}

The detail view controller is the last view controller in the split view and is also embedded in a navigation controller:

guard let navigationController = splitViewController.viewControllers.last 
  as? UINavigationController,
  let detailViewController = navigationController.topViewController else {
  fatalError("Missing detail view controller")
}

If I fail to find one of the view controllers I crash immediately as it means I have something wrong in my storyboard. Now we can configure the split view controller:

splitViewController.preferredDisplayMode = .allVisible
splitViewController.delegate = masterViewController

The displayModeButtonItem property of the split view controller returns a bar button item that we add to the navigation bar of the secondary view controller.

detailViewController.navigationItem.leftBarButtonItem = splitViewController.displayModeButtonItem
detailViewController.navigationItem.leftItemsSupplementBackButton = true

Presenting the Secondary

Since I am using a Storyboard segue we will take care of configuring the secondary view controller each time in prepare(for:sender:). If you are not using segues you would perform similar steps in the target-action method used to create and present the view controller:

override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
   guard let navController = segue.destination as? UINavigationController,
        let viewController = navController.topViewController as? DetailViewController else {
            fatalError("Expected DetailViewController")
    }

    // Manage the display mode button
    viewController.navigationItem.leftBarButtonItem = splitViewController?.displayModeButtonItem
    viewController.navigationItem.leftItemsSupplementBackButton = true

    // Configure the secondary view controller
    viewController.detailItem = Date()
}

The key step for the split view controller is to add the display mode button as a navigation item to the secondary view controller.

Collapsing the Secondary

There is one potential problem left. On initial startup in a horizontal compact size class such as the iPhone the split view controller collapses and will by default show the secondary view controller. This does not always make for a good user experience if we are depending on the user first doing something in the master view.

Luckily there is a delegate method we can use to control whether the collapsed split view shows the master or detail view controllers. In our master view controller:

extension MasterViewController: UISplitViewControllerDelegate {
  func splitViewController(_ splitViewController: UISplitViewController,
  collapseSecondary secondaryViewController: UIViewController, 
  onto primaryViewController: UIViewController) -> Bool {

  guard let navigationController = secondaryViewController 
  as? UINavigationController,
  let detailViewController = navigationController.topViewController 
  as? DetailViewController else {
    // Fallback to the default
    return false
  }

  return detailViewController.detailItem == nil
  }
}

This method checks the detailItem of the detail view controller. If it’s not set we return true to disable the default behaviour of showing the detail view controller collapsed onto the master view controller.

Now when the split view controller collapses it will show the master view controller until we have set the detail item in the secondary controller.

Get The Code

What I hope you see with this example is how little code is required with a split view controller to get a highly adaptive user interface. The sample Xcode project includes both Swift and Objective-C versions if you want to take a closer look: