UIKit12 min readJul 1, 2026

Mastering UISplitViewController for Adaptive UI on iOS

UISplitViewController is a powerful UIKit container view controller designed to manage two or more child view controllers in a master-detail interface. It automatically adapts its presentation based on the available screen real estate, making it essential for building responsive iOS applications that shine on both iPad and iPhone.

Introduction to UISplitViewController

The UISplitViewController is a crucial component in the UIKit framework for building adaptive user interfaces, particularly for applications targeting a wide range of iOS devices. Its primary role is to present one or more content view controllers side-by-side when enough screen space is available, typically on iPads or large iPhones in landscape orientation. When space is constrained, such as on iPhones in portrait or compact environments, it gracefully adapts its presentation, often collapsing into a single-column navigation stack.

Historically, UISplitViewController was the foundation for iconic iPad apps like Mail and Settings, providing a clear master-detail workflow. With iOS 14, Apple introduced UISplitViewController.Style, significantly modernizing its API and making it even more flexible for displaying multiple columns of content, not just two. This allows you to create highly dynamic and adaptive layouts that feel natural across all iOS device sizes, including the increasing variety of iPhone screen sizes and the traditional iPad experience. Understanding UISplitViewController is key to delivering a first-class user experience that leverages the full capabilities of each device.

Basic Setup: Two-Column vs. Three-Column Styles

UISplitViewController offers distinct presentation styles to accommodate different app requirements. The most common styles are split (for a traditional two-column master-detail) and tripleColumn (for a more complex three-column layout).

To initialize a UISplitViewController, you typically provide its child view controllers. For a two-column setup, you'll need a primary (master) and a secondary (detail) view controller. For a three-column setup, you'll add a compact/tertiary view controller. The order matters: the first view controller is the primary, the second is the secondary, and the third (if present) is the compact/tertiary.

Let's look at how to set up a basic two-column split view controller. This example sets up a primary 'Menu' view controller and a secondary 'Content' view controller. On iPads, they'll appear side-by-side. On iPhones, the primary will typically appear first, and selecting an item will push the secondary onto the navigation stack.

swift
import UIKit

class MasterViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = .systemGray5
        title = "Master"
        let label = UILabel()
        label.text = "Select an item"
        label.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(label)
        NSLayoutConstraint.activate([
            label.centerXAnchor.constraint(equalTo: view.centerXAnchor),
            label.centerYAnchor.constraint(equalTo: view.centerYAnchor)
        ])
    }
}

class DetailViewController: UIViewController {
    var itemText: String? {
        didSet {
            updateUI()
        }
    }
    private let label = UILabel()

    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = .systemBackground
        title = "Detail"
        label.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(label)
        NSLayoutConstraint.activate([
            label.centerXAnchor.constraint(equalTo: view.centerXAnchor),
            label.centerYAnchor.constraint(equalTo: view.centerYAnchor)
        ])
        updateUI()
    }

    private func updateUI() {
        label.text = itemText ?? "No Item Selected"
    }
}

func setupTwoColumnSplitView() -> UISplitViewController {
    let masterVC = MasterViewController()
    let detailVC = DetailViewController()

    let primaryNavController = UINavigationController(rootViewController: masterVC)
    let secondaryNavController = UINavigationController(rootViewController: detailVC)

    let splitVC = UISplitViewController(style: .split)
    splitVC.viewControllers = [primaryNavController, secondaryNavController]

    // Recommended for managing collapse behavior
    splitVC.setViewController(primaryNavController, for: .primary)
    splitVC.setViewController(secondaryNavController, for: .secondary)

    // Customize display mode (e.g., show both columns by default on iPad)
    splitVC.preferredDisplayMode = .automatic

    return splitVC
}

// Example usage in your SceneDelegate or AppDelegate (iOS 13+):
/*
if let windowScene = scene as? UIWindowScene {
    let window = UIWindow(windowScene: windowScene)
    window.rootViewController = setupTwoColumnSplitView()
    self.window = window
    window.makeKeyAndVisible()
}
*/

Understanding Display Modes and Preferred Column Widths

UISplitViewController offers several preferredDisplayMode options to control how its columns are presented. These modes dictate whether the primary column is always visible, hidden by default, or behaves automatically based on device orientation and size class.

  • .automatic: The split view controller automatically chooses the best display mode based on the environment.
  • .secondaryOnly: Only the secondary (detail) column is visible. The primary can be revealed by a swipe gesture or a bar button item.
  • .oneOverSecondary: The primary column is presented over the secondary column, obscuring it. This is common in compact environments where the primary acts as a menu.
  • .twoOverSecondary: Similar to .oneOverSecondary, but for three-column layouts, the compact column slides over the secondary.
  • .twoBesideSecondary: On three-column layouts, the compact column automatically appears alongside the secondary when space permits.

You can also influence the width of the columns using properties like preferredPrimaryColumnWidthFraction, preferredPrimaryColumnWidth, preferredSupplementaryColumnWidthFraction, and preferredSupplementaryColumnWidth. These properties allow you to fine-tune the layout to fit your app's content perfectly. Remember that these are preferred values, and the system might adjust them based on available space.

Setting a preferredPrimaryColumnWidthFraction of 0.3 means the primary column will attempt to occupy 30% of the split view controller's total width when in a side-by-side display mode.

swift
func setupCustomSplitView() -> UISplitViewController {
    let masterVC = MasterViewController()
    let detailVC = DetailViewController()

    let primaryNavController = UINavigationController(rootViewController: masterVC)
    let secondaryNavController = UINavigationController(rootViewController: detailVC)

    let splitVC = UISplitViewController(style: .split)
    splitVC.viewControllers = [primaryNavController, secondaryNavController]

    // Set a specific preferred display mode
    splitVC.preferredDisplayMode = .twoBesideSecondary

    // Set a preferred width for the primary column
    splitVC.preferredPrimaryColumnWidthFraction = 0.3 // Primary column takes 30% width
    splitVC.minimumPrimaryColumnWidth = 320 // Ensure primary column is at least 320 points wide
    splitVC.maximumPrimaryColumnWidth = 500 // Cap the primary column width at 500 points

    // This ensures that the detail view fills the space if only one VC is shown
    splitVC.presentsWithGesture = true // Allow swipe gesture to reveal primary

    return splitVC
}

Handling Collapse and Expand Behavior (Delegates)

When UISplitViewController transitions between showing multiple columns and a single column (e.g., rotating an iPad from landscape to portrait, or running on an iPhone), it's said to collapse or expand. You can gain fine-grained control over this behavior using the UISplitViewControllerDelegate protocol.

The most important delegate method is splitViewController(_:collapseSecondary:onto:). This method allows you to specify what happens to the secondary (detail) view controller when the split view collapses. By default, the secondary view controller is often pushed onto the navigation stack of the primary. You can customize this, for example, by discarding the secondary view controller or merging its content with the primary.

Another key method is splitViewController(_:separateSecondaryFrom:), which is called when the split view controller expands from a collapsed state. This is where you can recreate or properly configure your secondary view controller if it was discarded or modified during the collapse.

These delegate methods are crucial for maintaining state and ensuring a seamless user experience during device rotation and size class changes. For modern iOS development (iOS 14+), the default behavior of UISplitViewController is generally good, but the delegate provides escape hatches for complex scenarios.

swift
import UIKit

class CustomSplitViewController: UISplitViewController, UISplitViewControllerDelegate {

    override func viewDidLoad() {
        super.viewDidLoad()
        self.delegate = self
        self.preferredDisplayMode = .automatic

        let masterVC = MasterViewController()
        let detailVC = DetailViewController()

        let primaryNavController = UINavigationController(rootViewController: masterVC)
        let secondaryNavController = UINavigationController(rootViewController: detailVC)

        setViewController(primaryNavController, for: .primary)
        setViewController(secondaryNavController, for: .secondary)
    }

    // MARK: - UISplitViewControllerDelegate

    // This method is called when the split view controller is about to collapse.
    // Return true if you want the secondary view controller to be collapsed onto the primary.
    // Return false if you want to handle the collapse manually (e.g., discard secondary).
    // For iOS 14+, the default behavior is often sufficient.
    func splitViewController(
        _ splitViewController: UISplitViewController,
        collapseSecondary secondaryViewController: UIViewController,
        onto primaryViewController: UIViewController
    ) -> Bool {
        guard let secondaryNavController = secondaryViewController as? UINavigationController,
              let detailVC = secondaryNavController.topViewController as? DetailViewController else {
            return true // Let the system handle it if types don't match
        }

        // If the detail view controller isn't displaying unique content,
        // we might not want to show it when collapsing.
        if detailVC.itemText == nil {
            // Prevent the detail view from being pushed onto the primary's nav stack
            return true // Returning true means it will collapse onto primary (pushing detailVC if needed)
                        // Or you can try to remove it if you don't want it pushed
        }
        return false // If you return false, you MUST handle the collapse yourself,
                    // e.g., by pushing the detailVC onto the primary's nav stack MANUALLY
                    // or by simply returning control to the system to display primary only.
    }

    // This method is called when the split view controller is about to expand.
    // Use this to ensure the secondary view controller is correctly presented/setup.
    func splitViewController(
        _ splitViewController: UISplitViewController,
        separateSecondaryFrom primaryViewController: UIViewController
    ) -> UIViewController? {
        if let secondaryVC = splitViewController.viewController(for: .secondary) {
            return secondaryVC
        } else {
            // If secondary was discarded, recreate it here
            let newDetailVC = DetailViewController()
            let newSecondaryNavController = UINavigationController(rootViewController: newDetailVC)
            return newSecondaryNavController
        }
    }
}

Integrating with Navigation and Toolbar Items

When UISplitViewController collapses, it usually hides the primary column and only displays the secondary. To provide a way for users to access the primary column (e.g., a list of items), UISplitViewController automatically provides a displayModeButtonItem to the secondary's navigationItem.

You should attach this button item to your secondary view controller's navigation bar. This button, often appearing as a hamburger icon or back arrow, will toggle the primary column's visibility when the split view is in a collapsed state. This built-in functionality simplifies adapting your UI for different size classes.

For UISplitViewController with .split or .tripleColumn styles on iOS 14+, the displayModeButtonItem is more intelligently managed by the system. If you implement a UINavigationController as your secondary view controller, the split view controller takes care of inserting the button when necessary. If you're using a custom view controller arrangement, you might need to manually observe displayModeButtonItem changes or ensure your secondary view controller has a navigationItem where the item can be placed.

swift
import UIKit

class DetailViewControllerWithMenuButton: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = .systemBackground
        title = "Detail"

        // Ensure a navigation item exists to hold the button
        if let splitVC = splitViewController {
            // This button automatically toggles the primary column visibility
            // when the split view controller is collapsed.
            navigationItem.leftBarButtonItem = splitVC.displayModeButtonItem
            navigationItem.leftItemsSupplementBackButton = true
        }
    }
}

// To use this, replace DetailViewController with DetailViewControllerWithMenuButton
// in your split view setup.
func setupSplitViewWithToggle() -> UISplitViewController {
    let masterVC = MasterViewController()
    let detailVC = DetailViewControllerWithMenuButton()

    let primaryNavController = UINavigationController(rootViewController: masterVC)
    let secondaryNavController = UINavigationController(rootViewController: detailVC)

    let splitVC = UISplitViewController(style: .split)
    splitVC.viewControllers = [primaryNavController, secondaryNavController]
    splitVC.preferredDisplayMode = .automatic
    splitVC.delegate = splitVC.self as? UISplitViewControllerDelegate

    return splitVC
}

Advanced: Programmatic Display Mode Changes

While preferredDisplayMode and the displayModeButtonItem handle most scenarios, sometimes you might need to programmatically control the display mode. For instance, after a user selects an item in the primary column, you might want the split view to automatically hide the primary and show only the detail on an iPad, effectively mimicking the iPhone's navigation behavior.

You can achieve this by setting the displayMode property directly. For example, if you want to ensure only the secondary column is visible after a selection, you can set splitViewController.displayMode = .secondaryOnly. This is particularly useful in master-detail flows where the user has made a definite choice and the master list is no longer immediately needed.

It's important to understand the hierarchy of control: preferredDisplayMode is a preference, while displayMode is the current, active display state. Setting displayMode directly overrides preferredDisplayMode until the next collapse/expand event or until preferredDisplayMode takes effect again. Always ensure these programmatic changes enhance the user experience rather than confusing it.

swift
import UIKit

class MasterViewControllerWithSelection: UIViewController {
    weak var detailVC: DetailViewController? // Keep a weak reference to the detail

    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = .systemGray5
        title = "Master List"

        let showDetailButton = UIButton(type: .system)
        showDetailButton.setTitle("Show Item 1 Detail", for: .normal)
        showDetailButton.addTarget(self, action: #selector(showDetail), for: .touchUpInside)
        showDetailButton.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(showDetailButton)

        NSLayoutConstraint.activate([
            showDetailButton.centerXAnchor.constraint(equalTo: view.centerXAnchor),
            showDetailButton.centerYAnchor.constraint(equalTo: view.centerYAnchor)
        ])
    }

    @objc func showDetail() {
        detailVC?.itemText = "Item 1 Selected!"

        // If we are currently collapsed (e.g., on iPhone or iPad portrait),
        // the split view controller will handle pushing the detail.
        // If we are expanded (iPad landscape), we can optionally hide the primary.

        if let splitVC = splitViewController {
            // programmatically hide the primary pane after selection on wider screens
            // This is especially useful for three-column layouts where you want to
            // focus on the 'tertiary' column after selection.
            if splitVC.displayMode == .oneBesideSecondary || splitVC.displayMode == .twoBesideSecondary {
                 splitVC.hide(.primary)
            }
        }

        // On compact environments, detailVC might be pushed. Ensure it's visible.
        if let nav = detailVC?.navigationController, nav.topViewController != detailVC {
            nav.pushViewController(detailVC!, animated: true)
        }
    }
}

// For this to work, you'd integrate MasterViewControllerWithSelection
// and pass a reference to its detailVC:
func setupSplitViewWithProgrammaticDisplay() -> UISplitViewController {
    let masterVC = MasterViewControllerWithSelection()
    let detailVC = DetailViewController()
    masterVC.detailVC = detailVC // Pass reference

    let primaryNavController = UINavigationController(rootViewController: masterVC)
    let secondaryNavController = UINavigationController(rootViewController: detailVC)

    let splitVC = UISplitViewController(style: .split)
    splitVC.setViewController(primaryNavController, for: .primary)
    splitVC.setViewController(secondaryNavController, for: .secondary)
    
    // iOS 14+ specific way to assign view controllers correctly
    splitVC.setViewController(primaryNavController, for: .primary)
    splitVC.setViewController(secondaryNavController, for: .secondary)

    splitVC.preferredDisplayMode = .automatic

    return splitVC
}

Inconsistent UI on iPad vs. iPhone

Becoming a stronger iOS Engineer

THE MYTH or PROBLEM: Inconsistent UI on iPad vs. iPhone

Many developers struggle to make their apps feel native and adapt seamlessly across diverse iOS device sizes without writing entirely separate UIs for iPhone and iPad. This often leads to suboptimal user experiences or significant code duplication.

swift
// Common approach for distinct UIs:
if UIDevice.current.userInterfaceIdiom == .pad {
    // iPad-specific layout code
} else {
    // iPhone-specific layout code
}

TASK HIERARCHY: UISplitViewController's Adaptivity

UISplitViewController manages its child view controllers by leveraging size classes and display modes dynamically, rather than fixed device checks. It decides whether to present columns side-by-side or stack them based on the `horizontalCompact` (iPhone portrait) or `horizontalRegular` (iPad, iPhone landscape) environment.

UISplitViewController
Primary VC (e.g., Master List)
Secondary VC (e.g., Detail Content)
Supplementary VC (optional)
1

1. Initialization

Provide an array of child view controllers (primary, secondary, and optionally supplementary).

2

2. Determine Style

Choose between `.split` (two columns) or `.tripleColumn` (three columns) styles.

3

3. Evaluate Size Class

Based on the horizontal size class, decide if columns can be presented side-by-side or must collapse.

4

4. Apply Display Mode

Use `preferredDisplayMode` and `displayMode` to present columns, possibly triggering delegate methods for custom collapse/expand behavior.

5

5. Insert Display Button

Automatically or manually provide a `displayModeButtonItem` to toggle primary visibility in collapsed states.

Visualized execution hierarchy.

Powerful Guarantees

Automatic Adaptivity

Gracefully adapts layouts between compact and regular size classes without manual checks.

Consistent User Experience

Provides a familiar navigation pattern across all iOS devices.

Navigation Simplification

Handles much of the navigation logic for master-detail flows, reducing boilerplate.

REAL PRODUCTION EXAMPLE: Mail App Replication

A common bug arises when a user is viewing an email in detail on an iPad in landscape. If they rotate to portrait, the mail list disappears. If they then select a new email from the collapsed list, the *old* detail remains visible instead of the newly selected one, or the new detail is presented incorrectly.

Impact / Results
Seamless content transition
Correct detail shown after rotation
Preserved user context
THE FIX: Proper Delegate Implementation & State Management
swift
class CustomSplitVC: UISplitViewController, UISplitViewControllerDelegate {
    override func viewDidLoad() {
        super.viewDidLoad()
        self.delegate = self
        // ... setup view controllers ...
    }

    func splitViewController(
        _ splitViewController: UISplitViewController,
        collapseSecondary secondaryViewController: UIViewController,
        onto primaryViewController: UIViewController
    ) -> Bool {
        guard let secondaryNav = secondaryViewController as? UINavigationController, 
              let detailVC = secondaryNav.topViewController as? DetailViewController else {
            return false // Let system handle if types don't match or not a valid detail
        }
        
        // If current detail has NO content (e.g., nothing selected initially),
        // we can prevent it from being pushed onto the primary's nav stack.
        return detailVC.itemText == nil 
    }

    func splitViewController(
        _ splitViewController: UISplitViewController,
        separateSecondaryFrom primaryViewController: UIViewController
    ) -> UIViewController? {
        // Recreate or retrieve secondary if it was discarded/not present
        if let currentSecondary = splitViewController.viewController(for: .secondary) {
            return currentSecondary
        } else {
            // Important: ALWAYS return a valid secondary if system expects one
            return UINavigationController(rootViewController: DetailViewController())
        }
    }
}

INTERVIEW PERSPECTIVE

Common Question

How do you ensure state is preserved and navigation is consistent when UISplitViewController collapses and expands?

Strong Answer

A strong answer would emphasize using `UISplitViewControllerDelegate` methods, specifically `collapseSecondary(_:onto:)` and `separateSecondaryFrom(_:)`. Detail how you'd manage the lifecycle of the secondary view controller – ensuring it's either correctly pushed/popped onto the primary's navigation stack or recreated with its state preserved. Mention the importance of `displayModeButtonItem` for consistent navigation in collapsed states and adapting content based on `traitCollection` changes within child VCs.

Interviewers Expect you to understand:
  • Delegate protocol understanding
  • State preservation strategies
  • Adaptive UI considerations (size classes)
  • `displayModeButtonItem` usage
KEY TAKEAWAY

`UISplitViewController` is the primary tool for building robust, adaptive layouts on iOS. Embrace its delegate methods and style options to create a truly native and flexible user experience across all devices, minimizing code for responsive design.

Frequently Asked Questions

What is the main purpose of UISplitViewController?
The main purpose of `UISplitViewController` is to manage multiple view controllers in a master-detail interface, adapting their presentation automatically based on the available screen space. This allows for optimal layouts on both large screens (like iPads) and compact screens (like iPhones).
What are the key differences between the `split` and `tripleColumn` styles?
The `.split` style creates a traditional two-column layout (primary and secondary), ideal for basic master-detail. The `.tripleColumn` style supports three columns (primary, supplementary/compact, and secondary/detail), enabling more complex hierarchical navigation or showing related content alongside the main detail, especially useful on larger iPad Pros.
How do I make the primary column visible on an iPhone after it collapses?
When `UISplitViewController` collapses on an iPhone, the primary column is typically hidden, and you see only the secondary. To show the primary again, you should typically use the `displayModeButtonItem` provided by `UISplitViewController`. Assign this button to the `leftBarButtonItem` of your secondary view controller's `navigationItem`. It often appears as a back arrow or a menu icon, allowing the user to navigate back to the primary list.
When should I use `preferredDisplayMode` versus directly setting `displayMode`?
`preferredDisplayMode` is a hint to the split view controller about how it *should* present its columns under certain conditions (e.g., size classes). It's a preference the system tries to honor. `displayMode`, on the other hand, is the *current* active presentation. You should use `preferredDisplayMode` for general configuration and `displayMode` for programmatic, temporary changes, like forcing the primary pane to hide after an item selection.
What is the importance of `UISplitViewControllerDelegate`?
The `UISplitViewControllerDelegate` protocol is crucial for customizing how the split view controller behaves during collapse and expand transitions. It allows you to control which view controller is shown when collapsing (e.g., pushing the secondary onto the primary's navigation stack) and how new view controllers are created or retrieved when expanding.
#UIKit#UISplitViewController#iOS#Adaptive UI#iPad#iPhone