Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Swift UISplitViewController how to go from triple to double columns

I'm having a lot of trouble figuring out how to structure a UISplitViewController.

I want:

  • A sidebar in the primary view (always)
  • I want the 1st sidebar navigation item (animals) to show triple (sidebar, animal list, animal detail)
  • I want the 2nd sidebar navigation item (profile) to show double (sidebar, profile view)

I see other apps doing this (GitHub for example), but I've really got no idea how they're managing it. Resources are hard to find, and most tutorials I've seen just show one or the other column styles.

I'm mostly looking for answers on how to architecture this well, but any code would also be massively appreciated!

SceneDelegate

func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
    guard let windowScene = (scene as? UIWindowScene) else { return }
    window = UIWindow(frame: windowScene.coordinateSpace.bounds)
    window?.windowScene = windowScene
    window?.rootViewController = ViewController(style: .tripleColumn)
    window?.makeKeyAndVisible()
}

Root view controller

class ViewController: UISplitViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        
        viewControllers = [
            SidebarViewController(),
            AnimalsViewController(),
            AnimalDetailViewController()
        ]
        
        // Example attempt at removing the secondary view
        setViewController(ProfileViewController(), for: .supplementary)
        setViewController(nil, for: .secondary)
        hide(.secondary)
    }
}

Desired behaviour

Animals Triple example

Profile Double

Cheers!

like image 687
lacking-cypher Avatar asked Sep 19 '25 05:09

lacking-cypher


1 Answers

There is no "official" way to do it but it is possible. As far as I can tell, one of the best ways so solve it is to have two instances of UISplitViewController in your root view controller and juggle between them when needed. Here is my approach (approximately):

Disclaimer: This code was consulted with Apple engineers during the last WWDC22 on UIKit Labs. They have confirmed that it is very unfortunate that they currently do not offer a convenient way of doing it, and that this approach is probably the best way to do it. Feedback was filed and its ID passed to the engineers so hopefully we get an official API in the iOS 17 :D

rdar://FB10140263

Step 1. Initialise the UISplitViewControllers

private lazy var doubleColumnSVC: UISplitViewController = {
    $0.primaryBackgroundStyle = .sidebar
    // setup your SVC here
    $0.setViewController(doubleColumnPrimaryNC, for: .primary)
    return $0
}(UISplitViewController(style: .doubleColumn))

private lazy var tripleColumnSVC: UISplitViewController = {
    $0.primaryBackgroundStyle = .sidebar
    // setup your SVC here
    $0.setViewController(tripleColumnPrimaryNC, for: .primary)
    return $0
}(UISplitViewController(style: .tripleColumn))

Step 2. Initialise your sidebar VC and two separate UINavigationControllers

I have found it to be the most reliable solution for swapping sidebar VC. With a single UINavigationController instance there was a bug that the sidebar would randomly not appear. Two instances solve this problem while still keeping a single SidebarVC with proper focus state and already laid out content.

// Sidebar is shared and swapped between two split views
private lazy var sideBar = YourSideBarViewController()
private lazy var doubleColumnPrimaryNC = UINavigationController(
    rootViewController: UIViewController()
)
private lazy var tripleColumnPrimaryNC = UINavigationController(
    rootViewController: UIViewController()
)

Step 3. Make a property to store currently displayed SVC

It will come in handy in the next step when toggling between the two instances.

private var current: UISplitViewController?

Step 4. Implement Toggling between two styles when needed

This function should be called every time you want to navigate to a different screen from sidebar.

private func toggleStyleIfNeeded(_ style: UISplitViewController.Style) {
    switch style {
    case .doubleColumn:
        // skip if the desired column style is already set up
        if current === doubleColumnSVC { return }
        // reassign current
        current = doubleColumnSVC

        // here add doubleColumnSVC as child view controller
        // here add doubleColumnSVC.view as subview

        // swap the sidebar
        doubleColumnPrimaryNC.setViewControllers([sideBar], animated: false)

        // here remove tripleColumnSVC from parent
        // here remove tripleColumnSVC.view from superview

    case .tripleColumn:
        // skip if the desired column style is already set up
        if current === tripleColumnSVC { return }
        // reassign current
        current = tripleColumnSVC

        // here add tripleColumnSVC as child view controller
        // here add tripleColumnSVC.view as subview

        // swap the sidebar
        tripleColumnPrimaryNC.setViewControllers([sideBar], animated: false)

        // here remove doubleColumnSVC from parent
        // here remove doubleColumnSVC.view from superview

    default:
        return
    }
    // If you are using UITabBarController for your compact style, assign it here
    current?.setViewController(tabBar, for: .compact)
}

In lines that start with "here add" you will need to write your own code. I have simplified the code sample to make it shorter.

Step 5. Enjoy your SVC with dynamic columns!

Now you are basically ready to go! With this simple helper method on your root VC (or whichever one that is handling the navigation and managing the SVCs) you will have all the power that you need to achieve what you wanted, which is a UISplitViewController with dynamic number of columns!

func setViewController(
    _ viewController: UIViewController,
    for column: UISplitViewController.Column,
    style: UISplitViewController.Style
) {
    toggleStyleIfNeeded(style)
    current?.setViewController(viewController, for: column)
}

We are using this approach in production for a few months now and it works great. The app supports iOS, iPadOS and Mac Catalyst. There are some things like customising the status bar style and getting consistent sidebar button experience a bit tricky to work perfectly but with some adjustments and help from the UISplitViewControllerDelegate everything is possible.

Good luck!

P.S. If anyone have walked this path before and is able to share suggestions, please do! I would love to learn more on how one could improve this dynamic split view experience both for users and developers.

like image 89
Witek Bobrowski Avatar answered Sep 22 '25 00:09

Witek Bobrowski