In previous versions of iOS I was able to call show on a UIAlertView in the App Delegate. More specifically, show was called in:
func applicationDidBecomeActive(application: UIApplication)
Since UIAlertViews disregarded the view hierarchy in most cases, this solution worked no matter where the user was in the app.
With the introduction of UIAlertController this problem becomes a little trickier. UIAlertController is now a subclass of UIViewController and needs to be presented just like any other UIViewController. While presenting the UIAlertController from the keyWindow's rootViewController works, it's not the ideal solution.
Does anyone have any ideas on replicating [UIAlertView show] functionality for a UIAlertController? Any way to show the UIAlertController on app active without traversing the view hierarchy?
I figured out a solution that I believe to be more elegant than the answer I posted previously. I'll copy and paste the answer I posted to a similar question. Follow the link at the bottom of my post if you just want to see the code.
The solution is to use an additional UIWindow.
When you want to display your UIAlertController:
Make your window the key and visible window (window.makeKeyAndVisible())
Just use a plain UIViewController instance as the rootViewController of the new window. (window.rootViewController = UIViewController())
Present your UIAlertController on your window's rootViewController
A couple things to note:
Your UIWindow must be strongly referenced. If it's not strongly referenced it will never appear (because it is released). I recommend using a property, but I've also had success with an associated object.
To ensure that the window appears above everything else (including system UIAlertControllers), I set the windowLevel. (window.windowLevel = UIWindowLevelAlert + 1)
Lastly, I have a completed implementation if you just want to look at that.
https://github.com/dbettermann/DBAlertController
Here's what I ended up with:
public class func visibleViewController() -> UIViewController? {
return self.visibleViewController(UIApplication.sharedApplication().keyWindow?.rootViewController?)
}
private class func visibleViewController(viewController: UIViewController?) -> UIViewController? {
if viewController?.presentedViewController == nil {
println("Visible view controller: \(viewController)")
return viewController
} else if let navigationController = viewController as? UINavigationController {
return self.visibleViewController(navigationController.topViewController)
} else if let tabBarController = viewController as? UITabBarController {
return self.visibleViewController(tabBarController.selectedViewController)
} else {
return self.visibleViewController(viewController?.presentedViewController)
}
}
Try This
UIAlertController * alert= [UIAlertController
alertControllerWithTitle:#"title"
message:#" Your message hear"
preferredStyle:UIAlertControllerStyleAlert];
UIAlertAction *okBtnAction = [UIAlertAction actionWithTitle:#"Ok" style:UIAlertActionStyleDefault handler:^(UIAlertAction *action){
//Do what action u want.
[alert dismissViewControllerAnimated:YES completion:nil];
}];
[alert addAction:okAction];
UIAlertAction *cancelAction = [UIAlertAction actionWithTitle:#"cancel" style:UIAlertActionStyleDefault handler:^(UIAlertAction *action){
[alert dismissViewControllerAnimated:YES completion:nil];
//do something when click button
}];
[alert addAction:Action];
UIViewController *vc = [[[[UIApplication sharedApplication] delegate] window] rootViewController];
[vc presentViewController:alert animated:YES completion:nil];
Try using JSAlertView which handles both UIAlertView and UIAlertController APIs.
It provides short and easy methods for displaying alerts and handles multiple alerts fired at same time, very well.
Related
Two View Controllers in my app subclass the same class that has common methods. I would like to launch an alertController (fire an alert) from code in a method of this common sub-classed controller. But the following code is not launching anything.
Can anyone suggest the right way to point to the subclassed VC to get the alertController to launch?
Thanks in advance for any suggestions.
View controller1 wired to storyboard subclasses common class as follows:
#interface IDManageItemsVC : IDCommonVC <UIAlertViewDelegate>
The common VC subclasses CoreVC which has even more common methods for the whole app:
#import "IDCoreVC.h"
#interface IDCommonVC : IDCoreVC<UITableViewDelegate,UITableViewDataSource,UIAlertViewDelegate>
I am trying to fire the alert from code in commonVC (the superclass for the class wired to storyboard) as follows:
-(void)fireAlert {
UIAlertController *alert = [UIAlertController alertControllerWithTitle:#"Delete?" message:nil preferredStyle:UIAlertControllerStyleAlert];
UIAlertAction* yesButton = [UIAlertAction
actionWithTitle:#"OK"
style:UIAlertActionStyleDefault
handler:^(UIAlertAction * action)
{
//run code
}];
UIAlertAction* noButton = [UIAlertAction
actionWithTitle:#"Not Now"
style:UIAlertActionStyleDefault
handler:^(UIAlertAction * action)
{
//if chosen run code
}];
[alert addAction:noButton];
[alert addAction:yesButton];
if ([alert respondsToSelector:#selector(setPreferredAction:)]) {
[alert setPreferredAction:yesButton];
}
/* following points to VC not in hierarchy so commented out
id rootViewController = [UIApplication sharedApplication].delegate.window.rootViewController;
if([rootViewController isKindOfClass:[UINavigationController class]])
{
rootViewController = ((UINavigationController *)rootViewController).viewControllers.firstObject;
}
if([rootViewController isKindOfClass:[UITabBarController class]])
{
rootViewController = ((UITabBarController *)rootViewController).selectedViewController;
}
[rootViewController presentViewController:alertInvite animated:YES completion:nil]; */
//Following does not do anything
[self presentViewController:alert animated:YES completion:nil];
}
Edit:
Using the following method, with a breakpoint, I visually verified that topViewController is the right one and then presented the alertview from it and it still did not display. The only thing I noticed is that when I visually examined the alertview, it appears blank with just a slight white curve in the upper left where a rounded corner might be against a white rectangle. So perhaps, there is something wrong with the way I'm creating the alertview.
UIViewController *currentTopVC = [self currentTopViewController];
currentTopVC.presentViewController.........
- (UIViewController *)currentTopViewController {
UIViewController *topVC = [[[[UIApplication sharedApplication] delegate] window] rootViewController];
while (topVC.presentedViewController) {
topVC = topVC.presentedViewController;
}
return topVC;
}
and instead of [self presentViewController..., the following:
UIViewController *currentTopVC = [self currentTopViewController];
[currentTopVC presentViewController:alertInvite animated:YES completion:nil];
I copied and pasted your code and replaced
[self presentViewController:alertInvite animated:YES completion:nil];
with
[self presentViewController:alert animated:YES completion:nil];
and it worked.
This doesn't look like a inheritance issue to me. Are you by any chance calling this method from viewDidLoad: or any method that gets called before the view controller is actually shown? If so, try calling it from viewDidAppear:
I am updating some UIAlertViews, deprecated since iOS 9.0 to UIAlertViewControllers.
With UIAlertView, it was possible to just throw an alert from any code being executed--even in a utility class or shared instance--with the simple line:
[alertView show];
So if I call a shared instance such as
- (void)didTapDeleteButton:(id)sender {
NSArray *itemsToDelete = [self.selectedIndexPathToContact allValues];
[[IDModel sharedInstance] deleteItems:itemsToDelete];
//which contains the code:
UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:#"Keep Archive Copy?"
message:nil
delegate:self
cancelButtonTitle:#"No"
otherButtonTitles:#"OK",nil];
alertInvite.alertViewStyle = UIAlertViewStyleDefault;
alertInvite.tag=100;
[alertView show];
everything worked fine.
However, with the UIAlertController, this is not allowed. If you put the following code in the method of a class accessible via shared instance, when you get to presentViewController, it throws an error:
UIAlertController *alertView = [UIAlertController alertControllerWithTitle:#"Delete Item?" message:nil preferredStyle:UIAlertControllerStyleAlert];
UIAlertAction* yesButton = [UIAlertAction actionWithTitle:#"OK" style:UIAlertActionStyleDefault handler:^(UIAlertAction * action) {
[alertView dismissViewControllerAnimated:YES completion:nil];
}];
UIAlertAction* noButton = [UIAlertAction actionWithTitle:#"Not Now" style:UIAlertActionStyleDefault handler:^(UIAlertAction * action) {
[alertView dismissViewControllerAnimated:YES completion:nil];
}];
[alertView addAction:noButton];
[alertView addAction:yesButton];
if ([alertView respondsToSelector:#selector(setPreferredAction:)]) {
[alertView setPreferredAction:yesButton];
}
//FOLLOWING THROWS ERROR
[self presentViewController:alertView animated:YES completion:nil];
on the last line, that the class (reached via a shared instance) does not have this method. It seems you must use a more complicated way to throw alert. I've seen some somwehat convoluted approaches such as the following:
id rootViewController = [UIApplication sharedApplication].delegate.window.rootViewController;
if([rootViewController isKindOfClass:[UINavigationController class]])
{
rootViewController = ((UINavigationController *)rootViewController).viewControllers.firstObject;
}
if([rootViewController isKindOfClass:[UITabBarController class]])
{
rootViewController = ((UITabBarController *)rootViewController).selectedViewController;
}
[rootViewController presentViewController:alertInvite animated:YES completion:nil];
However, this does not work for me as I don't think my shared instance has a rootviewcontroller. Can anyone suggest a simple, straightforward way to do this?
I created an extension on UIViewController that allows me to create a new window and present a view controller from there. This allows me to present from any class, not just a view controller. Also, it prevents issues where you try to display an alert view from a view controller that is already presenting a view controller.
extension UIViewController {
func presentFromNewWindow(animated: Bool = true, completion: (() -> Void)? = nil) {
let window = newWindow()
if let rootViewController = window.rootViewController {
window.makeKeyAndVisible()
rootViewController.present(self, animated: animated, completion: completion)
}
}
private func newWindow() -> UIWindow {
let window = UIWindow(frame: UIScreen.main.bounds)
let rootViewController = UIViewController()
rootViewController.view.backgroundColor = .clear
window.backgroundColor = .clear
window.rootViewController = rootViewController
window.windowLevel = UIWindowLevelAlert
return window
}
}
You can then use this method to present your Alert Controller (or any UIViewController):
alertViewController.presentFromNewWindow()
When you dismiss the alert view controller, it is removed from the view controller and the window is removed from the hierarchy.
to show an alert from any code I could think of doing:
UIAlertController *alert = [UIAlertController alertControllerWithTitle:#"Alert" message:#"message" preferredStyle:UIAlertControllerStyleAlert];
[alert addAction: [UIAlertAction actionWithTitle:#"Yes" style:UIAlertActionStyleDefault handler:nil]];
alert.TitleColor = [UIColor whiteColor];
id<UIApplicationDelegate> delegate = [UIApplication sharedApplication].delegate;
UIViewController *vc = delegate.window.rootViewController;
[vc presentViewController:alert animated:YES completion:nil];
Note:
Note that in most cases I would NOT do this.
Non ui code shouldn't do ui! Which is likely also part of the reason why apple made the change: It encourages a proper model||view separation
Your question is quite vague, I believe. But I think what you've been looking for is an example of presenting a UIAlertController. Is that correct? If so, continue reading...
Example:
UIAlertController *alert = [UIAlertController alertControllerWithTitle:#"Alert" message:#"message" preferredStyle:UIAlertControllerStyleAlert];
[alert addAction: [UIAlertAction actionWithTitle:#"Yes" style:UIAlertActionStyleDefault handler:nil]];
alert.TitleColor = [UIColor whiteColor];
[self presentViewController:alert animated:YES completion:nil];
doc: https://developer.apple.com/documentation/uikit/uialertcontroller
I need to display a UIAlertController coming from the AppDelegate whenever I received a message from the topic.
I found this code on how to show a UIAlertController from the AppDelegate.
UIWindow* topWindow = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
topWindow.rootViewController = [UIViewController new];
topWindow.windowLevel = UIWindowLevelAlert + 1;
UIAlertController *uiAlert= ...
topWindow.hidden = YES;
It has no problem showing the alert on any view in my storyboard. The problem is that whenever I received multiple messages from the Topic, it also shows the UIAlertController multiple times, creating a layer of UIAlerController making the background black.
I tried this code to fix the problem, but it didn't
if (![topWindow.rootViewController.presentedViewController isKindOfClass:[UIAlertController class]]) {
[topWindow makeKeyAndVisible];
[topWindow.rootViewController presentViewController:uiAlert animated:YES completion:nil];
}
What should be the condition to only present one UIAlertController if there's no current UIAlertController presented?
I'd suggest not doing it this way, from the AppDelegate. There is absolutely no need to mess with the rootViewController.
Suggested Approaches
You can take two approaches for this:
Create a BaseViewController, add the message showing method in it. Inherit all your ViewControllers from that BaseViewController and use the shared method.
Create a utility class, Add a class method which shows message. Add that utility class either to your pch file so that it could be accessed everywhere or you could just import it where ever you want to display the message. Don't forget to add another Class method which gets the currently Visible viewcontrller.
First Approach: Using a BaseViewController (Recommended)
If you are using the BaseViewController Approach, your method will look like this:
-(void) showMessageWithHeader:(NSString *)header
andBody:(NSString *)bodyMessage{
UIAlertController *alertController = [UIAlertController alertControllerWithTitle:header message:bodyMessage preferredStyle:UIAlertControllerStyleActionSheet];
UIAlertAction * okAction = [UIAlertAction actionWithTitle:#"OK" style:UIAlertActionStyleDefault handler:^(UIAlertAction * action) {
}];
[alertController addAction:okAction];
[self presentViewController:alertController animated: YES completion: nil];
}
You can use it like the following in any ViewController inherited from your base:
[self showMessageWithHeader:#"Alert"
andBody:#"This message was generated from base class"];
I'd personally recommend this approach. You should always show your messages from ViewControllers, not let's say, some manager Class. You can return the message to ViewController using a block or whatever and then display it using this method.
Second Approach: Using a shared utility Class.
If you, however, prefer the utility class approach (For example if you insist on showing message directly from your manager Class which obviously doesn't know which ViewController is currently visible), add a utilityClass, let's suppose it's called UIUtilities. Now add two Class methods to it:
+ (UIViewController *) getVisibleViewController{
UIViewController *topController = [UIApplication sharedApplication].keyWindow.rootViewController;
topController = [UIUtilities getVisibleViewControllerFrom:topController];
return topController;
}
+ (UIViewController *) getVisibleViewControllerFrom:(UIViewController *) vc {
if ([vc isKindOfClass:[UINavigationController class]]) {
return [UIUtilities getVisibleViewControllerFrom:[((UINavigationController *) vc) visibleViewController]];
} else if ([vc isKindOfClass:[UITabBarController class]]) {
return [UIUtilities getVisibleViewControllerFrom:[((UITabBarController *) vc) selectedViewController]];
} else {
if (vc.presentedViewController) {
return [UIUtilities getVisibleViewControllerFrom:vc.presentedViewController];
} else {
return vc;
}
}
}
Using + (UIViewController *) getVisibleViewController will return you the currently visible ViewController. With that part done, you can now add your class method to show messages on VisibleViewController:
+(void) showMessageWithHeader:(NSString *)header
andBody:(NSString *)bodyMessage{
UIAlertController *alertController = [UIAlertController alertControllerWithTitle:header message:bodyMessage preferredStyle:UIAlertControllerStyleActionSheet];
UIAlertAction * okAction = [UIAlertAction actionWithTitle:#"OK" style:UIAlertActionStyleDefault handler:^(UIAlertAction * action) {
}];
[alertController addAction:okAction];
if(![[UIUtilities getVisibleViewController] isKindOfClass:[UIAlertController class]]){
[[UIUtilities getVisibleViewController] presentViewController:alertController animated: YES completion: nil];
}
}
Now you can call this method from where ever you want to show a message like:
[UIUtilities showMessageWithHeader:#"Alert"
andBody:#"This message came from Utility class"];
This will show only one Alert at one time. If one Alert is already visible, others won't be displayed. I wouldn't do it this way but since that's what you want, oh well.
Now no matter how many messages you receive, they will all stack up on currently visible ViewController and you can simply dismiss them one by one (Or only one Alert will be visible at one time as you require, depending on the approach you take), without any additional hassle.
func checkIfAlertHasPresented() -> UIAlertController?
{
if var topController = UIApplication.sharedApplication().keyWindow?.rootViewController
{
while let presentedVC = topController.presentedViewController
{
topController = presentedVC
}
if topController is UIAlertController
{
return (topController as! UIAlertController)
}
else
{
return nil
}
}
return nil
}
During development of my iOS application, I would like to be able to show the user a dialog if a serious bug occurs.
on SO, found that this could be achieved with adding something like the following to my AppDelegate class
- (void) notifyUserAboutSeriousBugWithMessage:(NSString *)message {
UIAlertController* alert = [UIAlertController alertControllerWithTitle:#"Serious bug: Alert developer"
message:message
preferredStyle:UIAlertControllerStyleAlert];
UIAlertAction* defaultAction = [UIAlertAction actionWithTitle:#"Continue" style:UIAlertActionStyleDefault
handler:^(UIAlertAction * action) {}];
[alert addAction:defaultAction];
[self.window.rootViewController presentViewController:alert animated:YES completion:^{}];
}
However, when I run this, i get an error message saying I'm Attempting to present <A> on <B> whose view is not in the window hierarchy!
My application is using a storyboard and the Entry/Start point <B> is a Splash Screen (-viewController). [This is not contained in a NavigationController]
When I'm done with the splash screen, I use a Modal Segue to show "the rest" of the app.
So how can I workaround this? In my app delegate, I want to get some view controller that will be in the view hierarcy and show my Alert in it's view.
(The reason I want to put this in the app delegate is that I want to present error alerts from classes that are not view controllers)
Thanks!
That's probably because you already have a presented viewController on top of your rootViewController. You need to present your alert on top of the topmost view controller.
Here is how you can get the topmost controller :
UIViewController *topViewController = self.window.rootViewController;
while (topViewController.presentedViewController) {
topViewController = topViewController.presentedViewController;
}
You can then do :
[topViewController presentViewController:alert animated:YES completion:nil];
Issue
I'm having issues with UIAlertController. It's an iPad project, in that I have two classes, say First and Second.
I'm presenting Second from First, using a modal presentation. In Second Class I have a button, clicking on that I'm showing UIActionSheet to perform some actions. It works perfectly except if user clicks on the button rapidly the Second class is being dismissed.
Code
First VC:
- (IBAction)showNext:(id)sender
{
UIViewController *vc = [self.storyboard instantiateViewControllerWithIdentifier:#"SecondVC"];
[self presentViewController:vc animated:YES completion:nil];
}
Second VC:
- (IBAction)showActionSheet:(UIButton *)sender
{
UIAlertController *alertVC = [UIAlertController alertControllerWithTitle:nil
message:nil
preferredStyle:UIAlertControllerStyleActionSheet];
[alertVC addAction:[UIAlertAction actionWithTitle:#"LogOut"
style:UIAlertActionStyleDestructive
handler:^(UIAlertAction *action)
{
}]];
[alertVC addAction:[UIAlertAction actionWithTitle:#"Update"
style:UIAlertActionStyleDefault
handler:^(UIAlertAction *action)
{
}]];
[alertVC setModalPresentationStyle:UIModalPresentationPopover];
UIPopoverPresentationController *popPresenter = [alertVC popoverPresentationController];
popPresenter.sourceView = sender;
popPresenter.sourceRect = sender.bounds;
[self presentViewController:alertVC animated:YES completion:nil];
}
No other code in both classes. I don't know why it happens, Actually it happened to one of my Client project, So I created a test app with above code and it's also causing the issue.
Screen Capture
What I have tried:
I checked UIAlertController Class Reference
Did a search on Google,, nothing came up
Alternatives:
If I set Second class as my rootViewController, the issue won't be there (But I can't do that, I need to navigate back and forth)
If I disable the popover dismissal when touched outside, it can avoid the issue. But I need this workflow.
Can anybody please help me ? I'm totally lost at this moment, not getting any useful info.
Thanks in advance.
I don't know what is the issue, but I fixed it. I used popoverPresentationControllerShouldDismissPopover and returned YES.
- (BOOL)popoverPresentationControllerShouldDismissPopover:(UIPopoverPresentationController *)popoverPresentationController;
{
return YES;
}
Eureka:
It was an accident!!! I didn't get any other way to fix this issue, so I thought will do the second alternative. While writing I accidentally wrote YES instead of NO. But it worked, I don't know how it worked and what is the reason for that issue.