Presenting a modal controller without knowing the current view controller? - ios

Is there a way to present a view controller modally without knowing what the visible view controller view is? Basically sort of like you would show an alert view at any points in time.
I would like to be able to do something like:
MyViewController *myVC = [[MyViewController alloc] init];
[myVC showModally];
I'd like to be able to call this from anywhere in the app, and have it appear on top. I don't want to care about what the current view controller is.
I plan to use this to show a login prompt. I don't want to use an alert view, and I also don't want to have login presentation code throughout the app.
Any thoughts on this? Or is there maybe a better way to achieve this? Should I just implement my own mechanism and just place a view on top of the window?

Well, you can follow the chain.
Start at [UIApplication sharedApplication].delegate.window.rootViewController.
At each view controller perform the following series of test.
If [viewController isKindOfClass:[UINavigationController class]], then proceed to [(UINavigationController *)viewController topViewController].
If [viewController isKindOfClass:[UITabBarController class]], then proceed to [(UITabBarController *)viewController selectedViewController].
If [viewController presentedViewController], then proceed to [viewController presentedViewController].

My solution in Swift (inspired by the gist of MartinMoizard)
extension UIViewController {
func presentViewControllerFromVisibleViewController(_ viewControllerToPresent: UIViewController, animated flag: Bool, completion: (() -> Void)?) {
if let navigationController = self as? UINavigationController {
navigationController.topViewController?.presentViewControllerFromVisibleViewController(viewControllerToPresent, animated: flag, completion: completion)
} else if let tabBarController = self as? UITabBarController {
tabBarController.selectedViewController?.presentViewControllerFromVisibleViewController(viewControllerToPresent, animated: flag, completion: completion)
} else if let presentedViewController = presentedViewController {
presentedViewController.presentViewControllerFromVisibleViewController(viewControllerToPresent, animated: flag, completion: completion)
} else {
present(viewControllerToPresent, animated: flag, completion: completion)
}
}
}

This solution gives you the top most view controller so that you can handle any special conditions before presenting from it. For example, maybe you want to present your view controller only if the top most view controller isn't a specific view controller.
extension UIApplication {
/// The top most view controller
static var topMostViewController: UIViewController? {
return UIApplication.shared.keyWindow?.rootViewController?.visibleViewController
}
}
extension UIViewController {
/// The visible view controller from a given view controller
var visibleViewController: UIViewController? {
if let navigationController = self as? UINavigationController {
return navigationController.topViewController?.visibleViewController
} else if let tabBarController = self as? UITabBarController {
return tabBarController.selectedViewController?.visibleViewController
} else if let presentedViewController = presentedViewController {
return presentedViewController.visibleViewController
} else {
return self
}
}
}
With this you can present your view controller from anywhere without needing to know what the top most view controller is
UIApplication.topMostViewController?.present(viewController, animated: true, completion: nil)
Or present your view controller only if the top most view controller isn't a specific view controller
if let topVC = UIApplication.topMostViewController, !(topVC is FullScreenAlertVC) {
topVC.present(viewController, animated: true, completion: nil)
}
One thing to note is that if there's a UIAlertController currently being displayed, UIApplication.topMostViewController will return a UIAlertController. Presenting on top of a UIAlertController has weird behavior and should be avoided. As such, you should either manually check that !(UIApplication.topMostViewController is UIAlertController) before presenting, or add an else if case to return nil if self is UIAlertController
extension UIViewController {
/// The visible view controller from a given view controller
var visibleViewController: UIViewController? {
if let navigationController = self as? UINavigationController {
return navigationController.topViewController?.visibleViewController
} else if let tabBarController = self as? UITabBarController {
return tabBarController.selectedViewController?.visibleViewController
} else if let presentedViewController = presentedViewController {
return presentedViewController.visibleViewController
} else if self is UIAlertController {
return nil
} else {
return self
}
}
}

You could have this code implemented in your app delegate:
AppDelegate.m
-(void)presentViewControllerFromVisibleController:(UIViewController *)toPresent
{
UIViewController *vc = self.window.rootViewController;
[vc presentViewController:toPresent animated:YES];
}
AppDelegate.h
-(void)presentViewControllerFromVisibleViewController:(UIViewController *)toPresent;
From Wherever
#import "AppDelegate.h"
...
AppDelegate *delegate = [UIApplication sharedApplication].delegate;
[delegate presentViewControllerFromVisibleViewController:myViewControllerToPresent];
In your delegate, you're getting the rootViewController of the window. This will always be visible- it's the 'parent' controller of everything.

I don't think you necessarily need to know which view controller is visible. You can get to the keyWindow of the application and add your modal view controller's view to the top of the list of views. Then you can make it work like the UIAlertView.
Interface file: MyModalViewController.h
#import <UIKit/UIKit.h>
#interface MyModalViewController : UIViewController
- (void) show;
#end
Implementation file: MyModalViewController.m
#import "MyModalViewController.h"
#implementation MyModalViewController
- (void) show {
UIWindow *window = [[UIApplication sharedApplication] keyWindow];
// Configure the frame of your modal's view.
[window addSubview: self.view];
}
#end

Related

How to detect a modal view is visible globally in SwiftUI

In SwiftUI, there're couple of ways to present a modal view like .popover.
My background is I would like to present a UIKit modal view somewhere else rather than under the current view page with
private func presentGlobally(animated: Bool, completion: (() -> Void)?) {
var rootViewController = UIApplication.shared.rootViewController
while true {
if let presented = rootViewController?.presentedViewController {
rootViewController = presented
} else if let navigationController = rootViewController as? UINavigationController {
rootViewController = navigationController.visibleViewController
} else if let tabBarController = rootViewController as? UITabBarController {
rootViewController = tabBarController.selectedViewController
} else {
break
}
}
UIApplication.shared.rootViewController?.present(self, animated: animated, completion: completion)
}
The above approach does not work because SwiftUI gave an error of
`` [Presentation] Attempt to present <BuyersCircle_iOS.GlobalModalUIKit: 0x15b82f000> on <TtGC7SwiftUI19UIHostingControllerGVS_15ModifiedContentGS1_GS1_GS1_GS1_V16BuyersCircle_iOS11ContentViewGVS_30_EnvironmentKeyWritingModifierGSqCS2_11UserService___GS4_GSqCS2_11GlobalModal___GS4_GSqCS2_9CartModel___GS4_GSqCS2_19ReachabilityMonitor___GS4_GSqCS2_19PartialSheetManager___: 0x158715600> (from
So I'm thinking
If I can get the current SwiftUI modal view controller, so I can present the UIKit modal based on it. I don't know how to make it work.
In you algorithm you're calculating rootViewController but then presenting self to UIApplication.shared.rootViewController, not to the found rootViewController
Replace
UIApplication.shared.rootViewController?.present(self, animated: animated, completion: completion)
With
rootViewController?.present(self, animated: animated, completion: completion)

Swift - dismiss then pop VC

How would I dismiss a modal View Controller and also its parent that was pushed?
self.presentingViewController?.dismiss(animated: true, completion: {
self.parent?.navigationController?.popViewController(animated: true)
})
This only dismisses the top modal.
You can go another way.
First, you have UINavigationController in your Home view. So you can write an extension that will allow you to go to the controller, which is in the navigation stack.
I tried making an implementation like this:
extension UINavigationController {
func routingPath(for controller: UIViewController) -> [UIViewController] {
guard viewControllers.contains(controller) else {
return []
}
var result: [UIViewController] = []
for previousController in viewControllers {
result.append(previousController)
if controller === previousController {
break
}
}
return result
}
func performNavigation(toPrevious controller: UIViewController,
shouldDismissModals: Bool = true) {
let previousViewControllers = routingPath(for: controller)
guard !previousViewControllers.isEmpty else { return }
viewControllers = previousViewControllers
if shouldDismissModals, let _ = controller.presentedViewController {
controller.dismiss(animated: true, completion: nil)
}
}
}
Then you can make a special method for UIViewController:
extension UIViewController {
func returnBackIfPossible(to controller: UIViewController? = nil,
shouldDismissModals: Bool = true) {
navigationController?.performNavigation(toPrevious: controller ?? self,
shouldDismissModals: shouldDismissModals)
}
}
Then you need to pass a reference for a Home controller to all of the next controllers (or store it somewhere). Next, when needed, you can call to homeViewController?.returnBackIfPossible() method, which will close all modals and reset navigation stack.
What is non-modal parent exectly?
Is it a view controller pushed by the navigation controller?
If then, you must pop that view controller from navigation controller.

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.

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];

Present UIAlertController in the currently active UIViewController

I've got a timer showing an alert when finished.
This alert view should be presented in the view controller which the user is currently in.
My feeling is this could be accomplished much more effective than the following:
The way I'm doing this now is give an observer for a notification to each of my 5 view controllers as well as a method to create and present that alert.
Is there a way to only set up the alert once and then present it in the view controller that is currently active?
Here's my code:
// I've got the following in each of my view controllers.
// In viewDidLoad()
override func viewDidLoad() {
super.viewDidLoad()
NSNotificationCenter.defaultCenter().addObserver(self, selector: #selector(SonglistViewController.presentSleepTimerFinishedAlert(_:)), name: "presentSleepTimerFinishedAlert", object: nil)
}
func presentTimerFinishedAlert(notification: NSNotification) {
let alertController = UIAlertController(title: "Timer finished", message: nil, preferredStyle: UIAlertControllerStyle.Alert)
alertController.addAction(UIAlertAction(title: "OK", style: UIAlertActionStyle.Default, handler: nil))
presentViewController(alertController, animated: true, completion: nil)
}
Thanks a lot for any ideas!
extension UIApplication {
/// The top most view controller
static var topMostViewController: UIViewController? {
return UIApplication.shared.keyWindow?.rootViewController?.visibleViewController
}
}
extension UIViewController {
/// The visible view controller from a given view controller
var visibleViewController: UIViewController? {
if let navigationController = self as? UINavigationController {
return navigationController.topViewController?.visibleViewController
} else if let tabBarController = self as? UITabBarController {
return tabBarController.selectedViewController?.visibleViewController
} else if let presentedViewController = presentedViewController {
return presentedViewController.visibleViewController
} else {
return self
}
}
}
With this you can easily present your alert like so
UIApplication.topMostViewController?.present(alert, animated: true, completion: nil)
One thing to note is that if there's a UIAlertController currently being displayed, UIApplication.topMostViewController will return a UIAlertController. Presenting on top of a UIAlertController has weird behavior and should be avoided. As such, you should either manually check that !(UIApplication.topMostViewController is UIAlertController) before presenting, or add an else if case to return nil if self is UIAlertController
extension UIViewController {
/// The visible view controller from a given view controller
var visibleViewController: UIViewController? {
if let navigationController = self as? UINavigationController {
return navigationController.topViewController?.visibleViewController
} else if let tabBarController = self as? UITabBarController {
return tabBarController.selectedViewController?.visibleViewController
} else if let presentedViewController = presentedViewController {
return presentedViewController.visibleViewController
} else if self is UIAlertController {
return nil
} else {
return self
}
}
}
You can find the Top ViewController on the navigation stack and directly present the AlertController from there. You can use the extension method posted here to find the Top ViewController from anywhere in your application:
https://stackoverflow.com/a/30858591/2754727
It really depends on your navigation schema.
First of all you will need current VC. If you've got root view controller as navigation controller and don't show any modals you can get current VC from rootVC. If you've got mixed navigation. i.e. tabbar and then navigation controllers inside, with possible some modals form them you can write an extension on AppDelegate which will search and return current VC.
Now you should pin somewhere this timer class - it may be a singleton or just be pinned somewhere. Than in this timer class, when the timer ends you can look for current VC (using AppDelegate's extension method or referring to your root navigation controller) an present an alert on it.

Resources