iOS top level navigation controller from AppDelegate Swift - ios

I am using this line:
var rootViewController = self.window!.rootViewController as? UINavigationController
However, this only returns the root and not the top level nav controller. I am trying to push a view on top, but if the root controller is not the top level, The view is not on top.
I am trying to find out the navigation controller of the current view controller.
This is all to solve my problem of this error:
Warning: Attempt to present <UINavigationController: 0x7f8473d7c800> on <UINavigationController: 0x7f8472022600> whose view is not in the window hierarchy!
Any suggestions?

You're nearly there :
func topViewController() -> UIViewController? {
if let rootViewController = self.window?.rootViewController as? UINavigationController {
return rootViewController.topViewController
}
return nil
}
Just be careful because there might also be another ViewController presented modally on top of your topViewController. You might want to check the topViewController.presentedViewController as well :
func topViewController() -> UIViewController? {
if let rootViewController = self.window?.rootViewController as? UINavigationController {
if let topVC = rootViewController.topViewController {
if let modalTopVC = topVC.presentedViewController {
return modalTopVC
}
return topVC
}
return rootViewController
}
return nil
}

Related

Get a reference to the last seen VC before backgrounding in scene delegate

I have a rootViewController as UITableViewController, where when a cell gets tapped it presents a modal VC. In those VC's I'm doing animations with UIViewPropertyAnimator and the issue is when the app gets sent to the background while currently displaying that VC because the animators are not stopped correctly and my viewWillDissapear is not called where I'm stopping the animators.
The question would be how to get a reference to a modal VC in my
sceneDidEnterBackground so that I can stop the animators correctly?
If I wanted to get a reference to my rootVC its easy enough like window.rootViewController but what about a modal VC?
I assume your modal VC IS the TOPMOST screen, and I think it should be the topmost screen you want to get reference from.
And if so, there are lots of references out there for that. But here's the whole function that I used the last time I needed it.
var windowScene: UIScene? = {
let windowScene = UIApplication.shared
.connectedScenes
.filter { $0.activationState == .foregroundActive }
.first
return windowScene
}()
var windowRootController: UIViewController? {
if let window = windowScene as? UIWindowScene {
return window.windows.last?.rootViewController
}
return UIApplication.shared.windows.filter {$0.isKeyWindow}.first?.rootViewController
}
/// Category for any controller.
extension UIViewController {
/// Class function to get the current or top most screen.
class func current(controller: UIViewController? = windowRootController) -> UIViewController? {
guard let controller = controller else { return nil }
if let navigationController = controller as? UINavigationController {
return current(controller: navigationController.visibleViewController)
}
if let tabController = controller as? UITabBarController {
if let selected = tabController.selectedViewController {
return current(controller: selected)
}
}
if let presented = controller.presentedViewController {
return current(controller: presented)
}
return controller
}
}
So basically, just call it like let modalVC = UIViewController.current().
Play with this piece of codes, like I'm pretty sure you won't need the windowScene and windowRootViewController, not 100% sure though. Try it. Hope it helps!

Correct way to grab the current navigation controller

Given that I have rootViewController which is UIApplication.shared.delegate?.window??.rootViewController, I want to grab the active navigation controller, if any.
So far what I've come up with:
guard var controller = rootViewController?.presentedViewController else { return rootViewController as? UINavigationController }
while let presented = controller.presentedViewController {
controller = presented
}
controller = controller.navigationController ?? controller
return controller as? UINavigationController
Is this sufficient? A co-working gave me this solution but the part I don't understand is rootViewController?.presentedViewController. Shouldn't it be rootViewController?.presentingViewController?
Use the below extension to grab the top most or current visible UIViewController and UINavigationController.
extension UIApplication {
class func topViewController(_ viewController: UIViewController? = UIApplication.shared.keyWindow?.rootViewController) -> UIViewController? {
if let nav = viewController as? UINavigationController {
return topViewController(nav.visibleViewController)
}
if let tab = viewController as? UITabBarController {
if let selected = tab.selectedViewController {
return topViewController(selected)
}
}
if let presented = viewController?.presentedViewController {
return topViewController(presented)
}
return viewController
}
class func topNavigationController(_ viewController: UIViewController? = UIApplication.shared.keyWindow?.rootViewController) -> UINavigationController? {
if let nav = viewController as? UINavigationController {
return nav
}
if let tab = viewController as? UITabBarController {
if let selected = tab.selectedViewController {
return selected.navigationController
}
}
return viewController?.navigationController
}
}
How to use?
let objViewcontroller = UIApplication.topViewController()
OR
let objNavigationController = UIApplication.topNavigation()
Is this sufficient?
Only you know that. Does it work reliably in your situation? If yes, it might be sufficient for your needs. But it's not the generally correct answer.
A much more reliable method is, as #vivekDas mentioned in a comment, to use the navigationController method, which returns the nearest view controller in the graph that's a navigation controller.
...the part I don't understand is rootViewController?.presentedViewController. Shouldn't it be rootViewController?.presentingViewController?
No. Let's say you've got two view controllers, a and b, and a presents b. In that case, a.presentedViewController is b, and b.presentingViewController is a. So rootViewController.presentedViewController is the view controller that rootViewController presents. rootViewController.presentingViewController would be the controller that presented rootViewController. But rootViewController is the root of the view controller object graph; by definition it hasn't been presented by any other view controller, so rootViewController.presentingViewController will always be nil.

Can a navigation controller and its top view controller have a presented view controller at the same time?

I need to find the top most navigation controller of my view controller hierarchy. I couldn't figure for sure if a navigation controller AND it's top view controller can have presented view controllers at the same, i.e
NavigationController --Presented--> UIViewController A
|
|
NavigationController.topViewController --Presented--> UIViewController B
Is this possible simultaneously? As in would i have to traverse both paths to the end and compare which is longer and then choose the correct path?
What I tried
I tried to simultaneously present view controllers on a navigation controller and its top view controller but i get this warning in LLDB
"Attempt to present on whose view is not in the window hierarchy!"
It didn't present the view controller (0x100605860) but will this ALWAYS be the case? Can custom presentations leave a view in the window hierarchy?
presentViewController shows a view controller. It doesn't return a view controller. If you're not using a UINavigationController, you're probably looking for presentedViewController and you'll need to start at the root and iterate down through the presented views.
Swift 3.*
extension UIApplication {
class func topViewController(controller: UIViewController? = UIApplication.shared.keyWindow?.rootViewController) -> UIViewController? {
if let navigationController = controller as? UINavigationController {
return topViewController(controller: navigationController.visibleViewController)
}
if let tabController = controller as? UITabBarController {
if let selected = tabController.selectedViewController {
return topViewController(controller: selected)
}
}
if let presented = controller?.presentedViewController {
return topViewController(controller: presented)
}
return controller
}
}
Swift 2
extension UIApplication {
class func topViewController(controller: UIViewController? = UIApplication.sharedApplication().keyWindow?.rootViewController) -> UIViewController? {
if let navigationController = controller as? UINavigationController {
return topViewController(navigationController.visibleViewController)
}
if let tabController = controller as? UITabBarController {
if let selected = tabController.selectedViewController {
return topViewController(selected)
}
}
if let presented = controller?.presentedViewController {
return topViewController(presented)
}
return controller
}
}
You can you use this anywhere on your controller
if let topController = UIApplication.topViewController() {
}

Get visibleViewController using UIWindow? [duplicate]

This question already has answers here:
How to find topmost view controller on iOS
(42 answers)
Closed 5 years ago.
So, I have self as UIWindow, but how can I get visibleViewController at current moment?
IN swift3:
func getVisibleViewController(_ rootViewController: UIViewController?) -> UIViewController? {
var rootVC = rootViewController
if rootVC == nil {
rootVC = UIApplication.shared.keyWindow?.rootViewController
}
if rootVC?.presentedViewController == nil {
return rootVC
}
if let presented = rootVC?.presentedViewController {
if presented.isKind(of: UINavigationController.self) {
let navigationController = presented as! UINavigationController
return navigationController.viewControllers.last!
}
if presented.isKind(of: UITabBarController.self) {
let tabBarController = presented as! UITabBarController
return tabBarController.selectedViewController!
}
return getVisibleViewController(presented)
}
return nil
}
You should check out this answer. The gist of it is that you start with the window's .rootViewController. In my own code (using a UINavigationController as the .rootViewController, I use this (in AppDelegate):
if let nvc = self.window?.rootViewController as? UINavigationController {
if let mvc = nvc.topViewController as? MasterViewController {
// ... do something
} else if let dvc = nvc.topViewController as? DetailViewController {
// ... do something
}
}
Note that if you are using the default template for a Master-Detail application, you will need to consider the SplitViewController which interposes itself, but that should be reasonably obvious from the boilerplate code.
If you add the child view controller:
let viewControllersVisible = self.rootViewController?.childViewControllers.filter({ $0.isVisible && $0.view.window })
This returns an array of UIViewControllers added in your view hierarchy, it doesn't say if the user is actually able to see those view controllers, depends on your hierarchy.
if you present modally just a view controller:
let viewControllerVisible = self.rootViewController?.presentedViewController
Inorder to get a reference to the top most view controller in the hierarchy try the following code
UIViewController *topController = [UIApplication sharedApplication].keyWindow.rootViewController;
while (topController.presentedViewController)
{
topController = topController.presentedViewController;
}
return topController;
If you want the topmost view on window try with this you will get the view.
[[[[UIApplication sharedApplication] keyWindow] subviews] lastObject];

How do I get the top view which is a presented view controller

I have a navigation bar controller. After some action i need to pop up a new view which is a presented view controller. Then How do i get the top view ?
I always getting top view on navigation stack but not the presented view. Why?
let appDelegate = UIApplication.sharedApplication().delegate as! AppDelegate
let aVariable = appDelegate.window
if let topController = aVariable!.visibleViewController() {
print(topController)
}
Try
if let topController = myNavigationController.visibleViewController {
print(topController)
}
Swift 3 example to get top UIViewController to present another UIViewController:
import UIKit
extension UIApplication {
var topVC: UIViewController? {
get {
let rootVC = UIApplication.shared.keyWindow?.rootViewController
if let nav = rootVC as? UINavigationController {
return nav.visibleViewController
} else if let tab = rootVC as? UITabBarController, let selected = tab.selectedViewController {
return selected
} else if let presented = rootVC?.presentedViewController {
return presented
}
return rootVC
}
}
}
Can call it with:
_ = UIApplication.shared.topVC?.present(yourViewController, animated: true)

Resources