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:
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.
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
}
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
}
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
This question already exists:
How to pass a value to Appdelegate from ViewController [closed]
Closed 3 years ago.
I am trying to get the value in ViewController from AppDelegate, but I am not able to do so.
I have only one ViewController. I tried to make the value as a constant or variable. None of them works.
I am not sure if this is the correct approach, but I tried to use rootViewController to access.
class ViewController: UIViewController {
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
var test:String = "test"
// let test = "test"
}
}
in AppDelegate
func application(_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
let viewController = self.window?.rootViewController as? ViewController
print("from_viewC \(String(describing: viewController?.test)))")
}
You have to declare the variable outside of viewWillAppear to make it accessible from outside:
class ViewController: UIViewController {
var test = "test"
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
// whatever is in here...
}
}
Set windows root view controller like this:
self.window.rootViewController = ViewController()
Than you can access your value as you are doing above. Also you have to make test variable a class member before you can compile.
Why did you try to access rootViewController's variable inside didFinishLaunchingWithOptions? What is your exact requirement?
DidFinishLaunchingWithOptions is the point where your rootViewController will get allocated!
You can try inside didEnterBackground, didEnterForeground, didBecomeActive delegate functions in appDelegate to access the rootViewController's variable!
I am very new to iOS development. In my app I have a tab bar and in one of the tabs I have a UISplitViewController. My issue is that when I go to the tab it shows the Detail view first. Then I have to click the back button to get the the master view. I have found one other person having this issue on stackoverflow, but the solution was in Objective-c and I am using the storyboard (not sure how to attach a class to it) and swift, so that did not help.
It also does not work when using an ipad in portrait mode. When I shift to landscape it works fine, but just shows a black screen (no back button) in portrait mode. Any help would be appreciated. Thanks.
I am not sure what other info you need or what you want me to show, so let me know if I left something out.
adjusted appdelegate
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
// Override point for customization after application launch.
let tabBarController = self.window!.rootViewController as! UITabBarController
let splitViewController = tabBarController.viewControllers![3] as! UISplitViewController
///////////////////Always visible property
splitViewController.preferredDisplayMode = .AllVisible
///////////////////
let navigationController = splitViewController.viewControllers[splitViewController.viewControllers.count-1] as! UINavigationController
navigationController.topViewController!.navigationItem.leftBarButtonItem = splitViewController.displayModeButtonItem()
splitViewController.delegate = self
return true
}
Update: ended up fixing following this answer Open UISplitViewController to Master View rather than Detail
I have created a sample SplitViewController in the project and set the property in the appdelegate. This works for me https://github.com/harsh62/stackoverflow_TestMasterDetailApp
splitViewController.preferredDisplayMode = .AllVisible
The full function is as follows:
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
// Override point for customization after application launch.
let splitViewController = self.window!.rootViewController as! UISplitViewController
///////////////////Always visible property
splitViewController.preferredDisplayMode = .AllVisible
///////////////////
let navigationController = splitViewController.viewControllers[splitViewController.viewControllers.count-1] as! UINavigationController
navigationController.topViewController!.navigationItem.leftBarButtonItem = splitViewController.displayModeButtonItem()
splitViewController.delegate = self
return true
}
References:
UISplitViewController - set always visible master controller when
How can I present a view controller from my AppDelegate and have a Navigation bar added to that view with a back button to the previous view? I need to do this programmatically from my AppDelegate. Currently I can push a controller from there, but it doesn't act like a segue. It doesn't add a nav bar with a back button. Now I know I should be able to add one myself, but when I do it gets hidden. Currently I'm using pushViewController(), but I imagine that's not the best way to do it.
I had something that I think is similar, if not the same:
HIGH LEVEL VIEW
The general composition of my App (thus far, and specific to the issue at hand - note: details about classes provided for context, not required for resolution) is as follows:
UIViewController (ViewController.swift) embedded in a UINavigationController
Buttons on UIViewController segue to a view with a custom class:
ExistingLocationViewController - subclass of:
UITableViewController
One of the buttons (Add New Location) in the UINavigationController's Toolbar segues to view with another custom class:
NewLocationViewController - subclass of:
UIViewController
CLLocationManagerDelegate
UITextFieldDelegate
There are a number of other items here, but I believe the above is sufficient as the foundation for the issue at hand
RESOLUTION
In order to preserve the navigation-bar (and tool-bar) going both forward and back - I have the following code in my custom classes (note: the following is Swift-3 code, you may have to adjust for Swift-2):
override func viewDidLoad() {
super.viewDidLoad()
//...
navigationController?.isNavigationBarHidden = false
navigationController?.isToolbarHidden = false
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated) // #=# not sure if this is needed
navigationController?.isNavigationBarHidden = false
navigationController?.isToolbarHidden = false
}
You could actually omit the last two lines in viewWillDisappear, or perhaps even omit the entire override function
The net result (for me) was as depicted below:
Hope that helps.
If you want add a NavigationController in appDelegate you can do it like this,in this way,your viewcontroller is load from storyboard
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
// Override point for customization after application launch.
self.window = UIWindow(frame: UIScreen.mainScreen().bounds)
let vc = UIStoryboard(name: "Main", bundle: NSBundle.mainBundle()).instantiateViewControllerWithIdentifier("vc") as! ViewController
let nav = UINavigationController(rootViewController: vc)
self.window?.rootViewController = nav
self.window?.backgroundColor = UIColor.whiteColor()
self.window?.makeKeyAndVisible()
return true
}
From my appdelegate I need to execute a method that is from a viewcontroller.
Please, can anybody tell me how to easily have the possibility to call whatever method I need from my appdelegate?
A lot of questions regarding this argument but none of these are useful/complete/right, so please avoid to post URL to other topics: I already checked all questions but I really can't find any precise answer regarding doing this in Swift. :)
It depends on how you've arranged your view controllers but here's an example from a simple iPhone master/detail project.
let root : UINavigationController = self.window!.rootViewController! as UINavigationController
let master : MasterViewController = root.topViewController as MasterViewController
println("\(master.description)")
The way I did it was to search for the view controller I want, starting on AppDelegate's var window: UIWindow? and then going deeper until I find it. I originally tried to implement NSNotification but this is not recommended on swift (I think computed property is a good replace for that, but it don't work in this case).
This is how I did for my tab based application with a NavigationController on top of my ViewController:
if let rootViewController = self.window?.rootViewController as? UITabBarController
if let viewControllers = rootViewController.viewControllers {
for navigationController in viewControllers {
if let yourViewController = navigationController.topViewController as? YourCustomViewController {
if yourViewController.hasSomeFlag {
yourViewController.theMethod()
}
break
}
}
}
}
You can do it with notifications, just add observer to your viewcontroller, post a notification from your app delegate, this observer will catch it and run a function you specify.
this tutorial should help get you started: https://www.codefellows.org/blog/how-to-implement-an-nsnotification-observer-in-swift
Try this
#UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
func application(application: UIApplication!, didFinishLaunchingWithOptions launchOptions: NSDictionary!) -> Bool {
// Override point for customization after application launch.
return true
}
func customMethod()
{
}
}
Call custom method from ViewController
class ViewController: UIViewController {
let appDelegate = UIApplication.sharedApplication().delegate as AppDelegate
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
appDelegate.customMethod()
}