Display UIAlertController from UIView/NSObject class
Asked Answered
H

10

27

I have working iOS application In order to support iOS8, I am replacing UIAlertView/UIActionSheet with UIAlertController.

Problem :
For display UIAlertController I need presentViewController method of UIViewController class.
But UIAlertView is display from classes which are inherited from UIView or NSObject,
I can not get [self presentViewController...] method for obvious reason.

My Work :
I tried getting rootViewController form current window and display UIAlertController.

[[[UIApplication sharedApplication] keyWindow].rootViewController presentViewController ...]

but have some rotation problems like if my current view controller do not have rotation support it will rotate if UIAlertController is open.

Question :
Did any one faced same problem and have safe solution ?
if yes please provide me some example or give some guide

Holmann answered 26/8, 2014 at 11:44 Comment(1)
I have had this problem. See my SO answer here for the code to get the topmost view controller with which to present another view controller. I agree that for most cases it is bad practice to present a view controller from an object that is not a view controller, but sometimes you DO need to.Sixtasixteen
R
10

It looks like you are currently (pre-iOS8) triggering an alert view from within your view object. That's pretty bad practice, as in general alerts should be triggered from actions and logic. And that code should live in controllers.

I suggest you refactor your current code to move the logic that triggers the alert to the correct controller, and then you can easily upgrade to iOS 8 by using self as the controller.

If instead you're calling the alert from an outside object, then pass in the controller to the method that calls the alert. Somewhere upstream you must have knowledge of the controller.

Reek answered 26/8, 2014 at 12:15 Comment(4)
+1 for suggestion, you are right, place for calling UIAlertView is wrong but, in current state i can not refactor it, i found same problem in many place, waiting for suggestion with less modification, any ways thanks, will apply your logic if no other suggestion came.Holmann
Previously, UIAlertView has been working from any UIView or NSObject. For ex: Showing alert from UITableViewCell works. There is no need to show it from UIViewController instance. Now, UIAlertController presents only from UIViewController which is not easy to show alerts from cells.Tsarina
"in general alerts should be triggered from actions and logic. And that code should live in controllers." - I prefer to put my business logic away from the (view) controllers, in my model. The controllers should only handle the visual side of the views they... control. So this all just doesn't make sense.Corrasion
@Corrasion business logic lives in business classes, but display logic lives in display controllers. When a display controller calls business logic that returns an error, it is up to it to determine how to display it to the user. I hope that makes it clearer.Reek
C
45

I solved an essentially similar problem today. Like Jageen, I ran into a situation where I wanted to present a UIAlertController but from a non-UIViewController class. In my case, I wanted an alert to pop up when the failure block of a HTTP request is run.

This is what I used and unlike our friend here, it worked quite perfectly for me.

UIApplication.sharedApplication().keyWindow?.rootViewController?.presentViewController(errorAlert, animated: true, completion: nil)
Clubby answered 5/1, 2015 at 6:37 Comment(3)
Thanks. However what do you think about what Rikkles said above? That your paradigm is really broken?Outpatient
This will not work when rootViewController is already presenting something else.Declarative
This is a no-op for me.Nunuance
L
15

The better solution for UIView classes is below

ObjectiveC

UIViewController *currentTopVC = [self currentTopViewController];
currentTopVC.presentViewController......... 

- (UIViewController *)currentTopViewController
{
    UIViewController *topVC = [[[[UIApplication sharedApplication] delegate] window] rootViewController];
    while (topVC.presentedViewController)
    {
        topVC = topVC.presentedViewController;
    }
    return topVC;
}

Swift

var topVC = UIApplication.sharedApplication().keyWindow?.rootViewController
while((topVC!.presentedViewController) != nil){
     topVC = topVC!.presentedViewController
}
topVC?.presentViewController........
Lucilla answered 8/7, 2015 at 9:54 Comment(1)
This is great - but I found that I could do it a bit more simply: in my AlertViewController i simply added this code: UIViewController *topVC = [[[[UIApplication sharedApplication] delegate] window] rootViewController]; [topVC presentViewController:presentUpdatedDataAvailableAlert animated:YES completion:nil];Mev
I
13

My solution is below:

Swift

class alert {
    func msg(message: String, title: String = "")
    {
        let alertView = UIAlertController(title: title, message: message, preferredStyle: .Alert)

        alertView.addAction(UIAlertAction(title: "Done", style: .Default, handler: nil))

        UIApplication.sharedApplication().keyWindow?.rootViewController?.presentViewController(alertView, animated: true, completion: nil)
    }
}

Here is sample usage:

let Alert = alert()
Alert.msg("My alert (without title)")
Alert.msg("This is my alert", title: "Warning!")
Ibert answered 5/11, 2015 at 18:7 Comment(0)
R
10

It looks like you are currently (pre-iOS8) triggering an alert view from within your view object. That's pretty bad practice, as in general alerts should be triggered from actions and logic. And that code should live in controllers.

I suggest you refactor your current code to move the logic that triggers the alert to the correct controller, and then you can easily upgrade to iOS 8 by using self as the controller.

If instead you're calling the alert from an outside object, then pass in the controller to the method that calls the alert. Somewhere upstream you must have knowledge of the controller.

Reek answered 26/8, 2014 at 12:15 Comment(4)
+1 for suggestion, you are right, place for calling UIAlertView is wrong but, in current state i can not refactor it, i found same problem in many place, waiting for suggestion with less modification, any ways thanks, will apply your logic if no other suggestion came.Holmann
Previously, UIAlertView has been working from any UIView or NSObject. For ex: Showing alert from UITableViewCell works. There is no need to show it from UIViewController instance. Now, UIAlertController presents only from UIViewController which is not easy to show alerts from cells.Tsarina
"in general alerts should be triggered from actions and logic. And that code should live in controllers." - I prefer to put my business logic away from the (view) controllers, in my model. The controllers should only handle the visual side of the views they... control. So this all just doesn't make sense.Corrasion
@Corrasion business logic lives in business classes, but display logic lives in display controllers. When a display controller calls business logic that returns an error, it is up to it to determine how to display it to the user. I hope that makes it clearer.Reek
W
6

For Swift 4

UIApplication.shared.keyWindow?.rootViewController?.present(alert, animated: true, completion: nil)

For Swift 5

UIApplication.shared.windows.last?.rootViewController?.present(alert, animated: true)
Winglet answered 13/7, 2018 at 16:13 Comment(0)
S
4

I had a situation where a subview contains a button to dismiss it. I present an alert to confirm the action. It sends a message to the delegate - which is the view controller containing the subview - to remove the subview

Originally I presented a UIAlertView from a UIView. Refactoring for UIAlertController, since the UIAlertController can't present itself like a UIAlertView can, I came up with the following (in Swift; easily translated to ObjC):

Add a protocol to the subview:

protocol MySubviewDelegate {

    // called when user taps subview/delete button
    //   or, you could call it from a gesture handler, etc.
    func displayAlert(alert : UIAlertController)

    // called when user confirms delete from the alert controller
    func shouldRemoveSubview(sender : AnyObject)

}

Add a delegate for the subview, and add a handler for the button/gesture tap:

class MySubview : UIView {

    var subviewDelegate : MySubviewDelegate!

    ...

    func handleTap(sender : AnyObject) {

        // set up the alert controller here
        var alert = UIAlertController(title: "Confirm Delete", 
            message: "This action is permanent. Do you wish to continue?", 
            preferredStyle: UIAlertControllerStyle.Alert)

        // Cancel action 
        //   nil handler means "no action if Cancel button selected"
        alert.addAction(UIAlertAction(title: "Cancel",
            style: UIAlertActionStyle.Cancel,
            handler: nil))

        // Confirm action
        alert.addAction(UIAlertAction(title: "Confirm",
            style: UIAlertActionStyle.Default,
            handler: { (action : UIAlertAction!) -> Void in

                // call delegate method to perform confirmed action, - i.e. remove
                self.subviewDelegate.shouldRemoveSubview(self)
        }))

        // call delegate method to display alert controller
        //   send alert object to delegate
        self.subviewDelegate.displayAlert(alert)
    }
}

Set the calling UIViewController as the delegate of the subview, e.g., in its viewDidLoad() method, and include protocol methods:

class viewController : UIViewController, MySubviewDelegate {

    override func viewDidLoad() {

        super.viewDidLoad()

        self.subviewDelegate = self

        ...
    }

    func displayAlert(alert : UIAlertController) {

        presentViewController(alert, animated: true, completion: nil)
    }

    func shouldRemoveSubview(sender : AnyObject) {

        // cast as UIView / MySubview subclass
        var subview = sender as MySubview

       // remove the subview / perform the desired action
       subview.removeFromSuperview()

       ...
    }

  ...
}

This avoids the need to find the topmost view controller, or pass references to view controllers to subviews (other than in an object/delegate relationship).

Stepmother answered 8/2, 2015 at 21:12 Comment(0)
D
3

In Swift 3:

UIApplication.shared.keyWindow?.rootViewController?.present(alertView, animated: true, completion: nil)
Deficit answered 25/11, 2016 at 12:46 Comment(0)
U
0

For Display UIAlertController in NSObject Class use below Code.

    UIAlertController * popup =   [UIAlertController
                              alertControllerWithTitle:nil
                              message:nil
                              preferredStyle:UIAlertControllerStyleActionSheet];

    UIAlertAction* cancel = [UIAlertAction
                             actionWithTitle:@"Cancel"
                             style:UIAlertActionStyleCancel
                             handler:^(UIAlertAction * action) {
                                 [popup dismissViewControllerAnimated:YES completion:nil];
                             }];
    [popup addAction:cancel];

    UIViewController *rootViewController = [[Helper shareInstance] topViewController];
    [rootViewController presentViewController:popup animated:YES completion:nil];

// Put Below Method in Your Global Helper Class.

- (UIViewController *)topViewController {
  return [self topViewController:[UIApplication sharedApplication].keyWindow.rootViewController];
}

- (UIViewController *)topViewController:(UIViewController *)rootViewController {
    if (rootViewController.presentedViewController == nil) {
        return rootViewController;
    }

    if ([rootViewController.presentedViewController isMemberOfClass:[UINavigationController class]]) {
        UINavigationController *navigationController = (UINavigationController *)rootViewController.presentedViewController;
        UIViewController *lastViewController = [[navigationController viewControllers] lastObject];
        return [self topViewController:lastViewController];
    }

    UIViewController *presentedViewController = (UIViewController *)rootViewController.presentedViewController;
    return [self topViewController:presentedViewController];
}
Upstart answered 24/8, 2017 at 9:34 Comment(0)
J
0

In general, alerts should be handled in the view controller. Here's an example of the code required:

Swift 3

private func displayError(message: String) {
    let alertController = UIAlertController(title: nil, message: message, preferredStyle: .alert)
    let okayAction = UIAlertAction(title: "Okay", style: .default, handler: nil)
    alertController.addAction(okayAction)
    present(alertController, animated: true, completion: nil)
}
Johansen answered 24/8, 2017 at 9:51 Comment(0)
M
0

I know the question has been already answered... But As I am also looking for the same issue, but none of the above solutions worked for me.

So after doing many try and error finally, I found a very easy and sustainable solution.

    func showError(title: String?, error: String?) {

    DispatchQueue.main.async(execute: {

        let alert = UIAlertController(title: title, message: error, preferredStyle: UIAlertControllerStyle.alert)

        alert.addAction(UIAlertAction(title: "OK", style: .cancel, handler: nil))

        CommonMethods.instance.topMostController()?.present(alert, animated: true, completion: nil)

    })
}

static let instance = CommonMethods()

fileprivate func topMostController() -> UIViewController? {

    var presentedVC = UIApplication.shared.keyWindow?.rootViewController
    while let pVC = presentedVC?.presentedViewController {
        presentedVC = pVC
    }

    if presentedVC == nil {  }
    return presentedVC
}
Maurita answered 2/4, 2019 at 11:12 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.