Search code examples
iosswiftipaduisplitviewcontrollerios13

UISplitViewController will not correctly collapse at launch on iPad iOS 13


I am transitioning my app to iOS 13, and the UISplitViewController collapses onto the detail view, rather than the master at launch — only on iPad. Also, the back button is not shown - as if it is the root view controller.

My app consists of a UISplitViewController which has been subclassed, conforming to UISplitViewControllerDelegate. The split view contains two children — both UINavigationControllers, and is embedded in a UITabBarController (subclassed TabViewController)

In the split view viewDidLoad, the delegate is set to self and preferredDisplayMode is set to .allVisible.

For some reason, the method splitViewController(_:collapseSecondary:onto:) not being called.

In iOS 12 on iPhone and iPad, the method splitViewController(_:collapseSecondary:onto:) is correctly called at launch, in between application(didFinishLaunchingWithOptions) and applicationDidBecomeActive.

In iOS 13 on iPhone, the method splitViewController(_:collapseSecondary:onto:) is correctly called at launch, in between scene(willConnectTo session:) and sceneWillEnterForeground.

In iOS 13 on iPad, however, if the window has compact width at launch e.g. new scene created as a split view, the splitViewController(_:collapseSecondary:onto:) method is not called at all. Only when expanding the window to regular width, and then shrinking is the method called.

class SplitViewController: UISplitViewController, UISplitViewControllerDelegate {
override func viewDidLoad() {
        super.viewDidLoad()
        self.delegate = self
        preferredDisplayMode = .allVisible
}

func splitViewController(_ splitViewController: UISplitViewController, collapseSecondary secondaryViewController:UIViewController, onto primaryViewController:UIViewController) -> Bool {
        print("Split view controller function")
        guard let secondaryAsNavController = secondaryViewController as? UINavigationController else { return false }
        guard let topAsDetailController = secondaryAsNavController.topViewController as? DetailViewController else { return false }
        if topAsDetailController.passedEntry == nil {
            return true
        }
        return false
    }
}
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {

        // Setup split controller
        let tabViewController = self.window!.rootViewController as! TabViewController
        let splitViewController = tabViewController.viewControllers![0] as! SplitViewController
        let navigationController = splitViewController.viewControllers[splitViewController.viewControllers.count-1] as! UINavigationController
        navigationController.topViewController!.navigationItem.leftBarButtonItem = splitViewController.displayModeButtonItem
        navigationController.topViewController!.navigationItem.leftBarButtonItem?.tintColor = UIColor(named: "Theme Colour")

        splitViewController.preferredDisplayMode = .allVisible

}
class AppDelegate: UIResponder, UIApplicationDelegate {

    var window: UIWindow?

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        if #available(iOS 13.0, *) {
        } else {
            let tabViewController = self.window!.rootViewController as! TabViewController
            let splitViewController = tabViewController.viewControllers![0] as! SplitViewController
            let navigationController = splitViewController.viewControllers[splitViewController.viewControllers.count-1] as! UINavigationController
            navigationController.topViewController!.navigationItem.leftBarButtonItem = splitViewController.displayModeButtonItem
            navigationController.topViewController!.navigationItem.leftBarButtonItem?.tintColor = UIColor(named: "Theme Colour")

            splitViewController.preferredDisplayMode = .allVisible
        }

        return true
    }

It stumps me why the method is being called in iPhone, but not in iPad! I am a new developer and this is my first post, so apologies if my code doesn't give enough detail or is not correctly formatted!


Solution

  • For some reason on iOS 13 specifically on the iPad in compact traitCollections the call to the delegate to see if it should collapse is happening BEFORE viewDidLoad is called on the UISplitViewController and so when it makes that call, your delegate is not set, and the method never gets called.

    If you're creating your splitViewController programmatically this is an easy fix, but if you're using Storyboards not so much. You can work around this by setting your delegate in awakeFromNib() instead of viewDidLoad()

    Using your example from the original post, a sample of code would be as follows

    class SplitViewController: UISplitViewController, UISplitViewControllerDelegate {
        override func awakeFromNib() {
            super.awakeFromNib()
            delegate = self
            preferredDisplayMode = .allVisible
        }
    
        func splitViewController(_ splitViewController: UISplitViewController, collapseSecondary secondaryViewController:UIViewController, onto primaryViewController:UIViewController) -> Bool {
            return true
        }
    }
    

    You'll also want to make sure whatever logic you're using in the collapseSecondary function isn't referencing variables that aren't yet populated since viewDidLoad hasn't been called yet.