18

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!

Belacqua2000
  • 301
  • 2
  • 6
  • 1
    Please file a bug report with Apple. iOS 13 has had split view problems since the 1st beta and Apple still has not fixed them. Time is running out. – rmaddy Aug 29 '19 at 03:32
  • @rmaddy can you be more specific? what split view problems are new in iOS 13? – matt Sep 07 '19 at 23:00
  • 2
    @matt See http://www.openradar.me/radar?id=4969975819272192 – rmaddy Sep 07 '19 at 23:32
  • @rmaddy Thanks. I'm having some trouble imagining why anyone would do that (replace both view controllers of an existing split view controller). A workaround is to create and configure a new split view controller and substitute it for the old one (as the window's root view controller). – matt Sep 08 '19 at 00:04
  • @matt That's my fallback plan if Apple doesn't fix the bug. It's just so much simpler to call just `setViewControllers` than it is to create and configure a whole new split view. The really odd thing is it was fixed in iOS 13 beta 4 and then broken again in beta 5. I haven't tried 13.1 yet. – rmaddy Sep 08 '19 at 00:10
  • Thanks, I appreciate your sharing the example. Hope you'll keep me posted. By the way on my test of your github project on beta 7 the split view controller did NOT magically come back after rotating and rotating back. – matt Sep 08 '19 at 00:22
  • @matt As for rotating, it has to be a rotation that would switch between compact and regular width such that the split view goes from showing only one column to showing both. For iPhones, that's only on the iPhone X varieties I believe. For iPads, it depends on the width of the app given multi-tasking. – rmaddy Sep 08 '19 at 01:55
  • Yeah, confirmed on the 6s Plus simulator. – matt Sep 08 '19 at 02:41
  • This still seems to be a problem in iOS 13.1 -- or is it me? – ICL1901 Sep 26 '19 at 04:39
  • Yes, this is still an issue for me on iOS 13.1 on an iPad. – Forest Katsch Sep 26 '19 at 16:11

5 Answers5

7

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.

user2898617
  • 607
  • 7
  • 10
2

Well, I think the answer should cover the iOS14 now.

If you find the delegate method is not be called.

func splitViewController(_ splitViewController: UISplitViewController, collapseSecondary secondaryViewController:UIViewController, onto primaryViewController:UIViewController) -> Bool {
        ...
}

maybe you should consider to use iOS14's one.

  @available(iOS 14.0, *)
  func splitViewController(_ svc: UISplitViewController, topColumnForCollapsingToProposedTopColumn proposedTopColumn: UISplitViewController.Column) -> UISplitViewController.Column {
        return .primary
  }
Chen
  • 41
  • 3
1

I have an Xcode project - now for iOS 13 - that uses a tab bar controller with relationships to five split view controllers, each with their own master detail (table) views and controllers.

Previously - iOS 12.x and earlier, in fact back when I was writing Objective-C - my split view controller delegate was set in code of the master view controller of each (parent) split view controller - I set the delegate in the subclassed UITableViewController's viewDidLoad method. This worked successfully for years on both iPhone and iPad.

e.g.

class MasterViewController: UITableViewController, UISplitViewControllerDelegate {

    override func viewDidLoad() {
        super.viewDidLoad()
        splitViewController?.preferredDisplayMode = UISplitViewController.DisplayMode.allVisible
        splitViewController?.delegate = self
        ...
    }

    func splitViewController(_ splitViewController: UISplitViewController, collapseSecondary secondaryViewController:UIViewController, onto primaryViewController:UIViewController) -> Bool {
        ...
    }
}

To be clear, I have not subclassed the tab bar controller or the split view controllers.

With the release of Xcode 11 and iOS 13, the split view controller delegate methods in the master view controllers were no longer called.

To be clear, for iOS 13, regardless of device or simulator, splitViewController(_:collapseSecondary:onto:) is not called (tested using breakpoints), with the resulting behaviour:

  • iPhone - detail view controller is presented when app is run on device or simulator.
  • iPad - detail view controller is presented when app is run on device or simulator, without a back button, so there is no obvious mechanism to "escape" the detail view. The only user workaround I found that resolves this problem, is to change device orientation. Following that, the split view controller behaves as expected.

I thought this may have something to do with the new class SceneDelegate.

So I retrofitted a custom SceneDelegate class into my test projects and then my primary project.

I have the custom SceneDelegate class working perfectly. I know this because I successfully set a window?.tintColor in the scene(_:willConnectTo:options:) method.

However the problems with split view controller delegates continued.

I logged feedback to Apple and this is their edited response...

...the problem is that you are setting the UISplitViewController’s delegate in an override of viewDidLoad. It’s possible that the UISplitViewController is deciding to collapse before anything causes its view to be loaded. When it does that, it checks its delegate, but since the delegate is still nil since you haven’t set it yet, your code wouldn’t be called.

Since views are loaded on demand, the timing of viewDidLoad can be unpredictable. In general it’s better to set up things like view controller delegates earlier. Doing it in scene(willConnectTo: session) is likely to work better.

This advice helped me a lot.

In my custom SceneDelegate class I added the following code into the scene(_:willConnectTo:options:) method...

class SceneDelegate: UIResponder, UIWindowSceneDelegate, UISplitViewControllerDelegate {

    var window: UIWindow?

    func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {

        guard let window = window else { return }
        guard let tabBarController = window.rootViewController as? UITabBarController else { return }

        guard let splitViewController = tabBarController.viewControllers?.first as? UISplitViewController else { return }

        splitViewController.delegate = self
        splitViewController.preferredDisplayMode = UISplitViewController.DisplayMode.allVisible
    }

    ...

    func splitViewController(_ splitViewController: UISplitViewController, collapseSecondary secondaryViewController:UIViewController, onto primaryViewController:UIViewController) -> Bool {
        ...
    }

}

This code worked for both iPhone and iPad, but perhaps obviously for only the first split master detail view controller combination.

I changed the code to attempt to achieve this success for all five split view controllers...

    guard let window = window else { return }
    guard let tabBarController = window.rootViewController as? UITabBarController else { return }

    guard let splitViewControllers = tabBarController.viewControllers else { return }

    for controller in splitViewControllers {

        guard let splitViewController = controller as? UISplitViewController else { return }
        splitViewController.delegate = self
        splitViewController.preferredDisplayMode = UISplitViewController.DisplayMode.allVisible
    }

This code works too... almost...

My check for whether to return true for collapseSecondary is based on a unique value - a computed property - from each of the five detail view controllers. Because of this unique check, it seemed difficult to determine this in my custom SceneDelegate class, so in my custom SceneDelegate class, I wrote the following code instead...

    guard let window = window else { return }
    guard let tabBarController = window.rootViewController as? UITabBarController else { return }

    guard let splitViewControllers = tabBarController.viewControllers else { return }

    for controller in splitViewControllers {

        guard let splitViewController = controller as? UISplitViewController else { return }
        guard let navigationController = splitViewController.viewControllers.first else { return }
        guard let masterViewController = navigationController.children.first else { return }
        splitViewController.delegate = masterViewController as? UISplitViewControllerDelegate
        splitViewController.preferredDisplayMode = UISplitViewController.DisplayMode.allVisible
    }

...and then made each detail view controller conform to UISplitViewControllerDelegate.

e.g.

class MasterViewController: UITableViewController, UISplitViewControllerDelegate {

    override func viewDidLoad() {
        super.viewDidLoad()
        // the following two calls now in the scene(_:willConnectTo:options:) method...
        // splitViewController?.preferredDisplayMode = UISplitViewController.DisplayMode.allVisible
        // splitViewController?.delegate = self
        ...
    }

    func splitViewController(_ splitViewController: UISplitViewController, collapseSecondary secondaryViewController:UIViewController, onto primaryViewController:UIViewController) -> Bool {
        ...
    }
}

So far so good, each of the five split view controllers collapses the detail view at app startup, for both iPhone and iPad.

andrewbuilder
  • 3,629
  • 2
  • 24
  • 46
0

You need to add this in the function "scene" in the class "SceneDelegate":

splitViewController.delegate = self

for example:

    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

    splitViewController.delegate = self//<<<<<<<<add this

    }
Richter
  • 164
  • 2
  • 6
0
override func awakeFromNib() {
      super.awakeFromNib()
      splitViewController?.delegate = self
      splitViewController?.preferredDisplayMode = .allVisible
}

//MARK: Split View Controller Delegate
    func splitViewController(_ splitViewController: UISplitViewController, collapseSecondary secondaryViewController: UIViewController, onto primaryViewController: UIViewController) -> Bool {
        print("splitview collapse method")
        return false
    }

Method is called at launch in Xcode 12.4 on Simulator iPhone, if you set: Storyboard -> Split View Controller -> Style -> Unspecified (Discouraged) not Double Column

  • As it’s currently written, your answer is unclear. Please [edit] to add additional details that will help others understand how this addresses the question asked. You can find more information on how to write good answers [in the help center](/help/how-to-answer). – Community Jun 28 '22 at 16:17