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!
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.
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:
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 theUISplitViewController
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 inscene(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.
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
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With