How to present UIAlertController when not in a view controller?

前端 未结 30 3114
庸人自扰
庸人自扰 2020-11-22 06:21

Scenario: The user taps on a button on a view controller. The view controller is the topmost (obviously) in the navigation stack. The tap invokes a utility class method call

相关标签:
30条回答
  • 2020-11-22 06:50

    Kevin Sliech provided a great solution.

    I now use the below code in my main UIViewController subclass.

    One small alteration i made was to check to see if the best presentation controller is not a plain UIViewController. If not, it's got to be some VC that presents a plain VC. Thus we return the VC that's being presented instead.

    - (UIViewController *)bestPresentationController
    {
        UIViewController *bestPresentationController = [UIApplication sharedApplication].keyWindow.rootViewController;
    
        if (![bestPresentationController isMemberOfClass:[UIViewController class]])
        {
            bestPresentationController = bestPresentationController.presentedViewController;
        }    
    
        return bestPresentationController;
    }
    

    Seems to all work out so far in my testing.

    Thank you Kevin!

    0 讨论(0)
  • 2020-11-22 06:50

    You can try to implement a category on UIViewController with mehtod like - (void)presentErrorMessage; And and inside that method you implement UIAlertController and then present it on self. Than in your client code you will have something like:

    [myViewController presentErrorMessage];

    In that way you'll avoid unneccessary parametrs and warnings about view not being in window hierarchy.

    0 讨论(0)
  • 2020-11-22 06:52

    At WWDC, I stopped in at one of the labs and asked an Apple Engineer this same question: "What was the best practice for displaying a UIAlertController?" And he said they had been getting this question a lot and we joked that they should have had a session on it. He said that internally Apple is creating a UIWindow with a transparent UIViewController and then presenting the UIAlertController on it. Basically what is in Dylan Betterman's answer.

    But I didn't want to use a subclass of UIAlertController because that would require me changing my code throughout my app. So with the help of an associated object, I made a category on UIAlertController that provides a show method in Objective-C.

    Here is the relevant code:

    #import "UIAlertController+Window.h"
    #import <objc/runtime.h>
    
    @interface UIAlertController (Window)
    
    - (void)show;
    - (void)show:(BOOL)animated;
    
    @end
    
    @interface UIAlertController (Private)
    
    @property (nonatomic, strong) UIWindow *alertWindow;
    
    @end
    
    @implementation UIAlertController (Private)
    
    @dynamic alertWindow;
    
    - (void)setAlertWindow:(UIWindow *)alertWindow {
        objc_setAssociatedObject(self, @selector(alertWindow), alertWindow, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    }
    
    - (UIWindow *)alertWindow {
        return objc_getAssociatedObject(self, @selector(alertWindow));
    }
    
    @end
    
    @implementation UIAlertController (Window)
    
    - (void)show {
        [self show:YES];
    }
    
    - (void)show:(BOOL)animated {
        self.alertWindow = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
        self.alertWindow.rootViewController = [[UIViewController alloc] init];
    
        id<UIApplicationDelegate> delegate = [UIApplication sharedApplication].delegate;
        // Applications that does not load with UIMainStoryboardFile might not have a window property:
        if ([delegate respondsToSelector:@selector(window)]) {
            // we inherit the main window's tintColor
            self.alertWindow.tintColor = delegate.window.tintColor;
        }
    
        // window level is above the top window (this makes the alert, if it's a sheet, show over the keyboard)
        UIWindow *topWindow = [UIApplication sharedApplication].windows.lastObject;
        self.alertWindow.windowLevel = topWindow.windowLevel + 1;
    
        [self.alertWindow makeKeyAndVisible];
        [self.alertWindow.rootViewController presentViewController:self animated:animated completion:nil];
    }
    
    - (void)viewDidDisappear:(BOOL)animated {
        [super viewDidDisappear:animated];
        
        // precaution to ensure window gets destroyed
        self.alertWindow.hidden = YES;
        self.alertWindow = nil;
    }
    
    @end
    

    Here is a sample usage:

    // need local variable for TextField to prevent retain cycle of Alert otherwise UIWindow
    // would not disappear after the Alert was dismissed
    __block UITextField *localTextField;
    UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"Global Alert" message:@"Enter some text" preferredStyle:UIAlertControllerStyleAlert];
    [alert addAction:[UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) {
        NSLog(@"do something with text:%@", localTextField.text);
    // do NOT use alert.textfields or otherwise reference the alert in the block. Will cause retain cycle
    }]];
    [alert addTextFieldWithConfigurationHandler:^(UITextField *textField) {
        localTextField = textField;
    }];
    [alert show];
    

    The UIWindow that is created will be destroyed when the UIAlertController is dealloced, since it is the only object that is retaining the UIWindow. But if you assign the UIAlertController to a property or cause its retain count to increase by accessing the alert in one of the action blocks, the UIWindow will stay on screen, locking up your UI. See the sample usage code above to avoid in the case of needing to access UITextField.

    I made a GitHub repo with a test project: FFGlobalAlertController

    0 讨论(0)
  • 2020-11-22 06:52

    The following solution did not work even though it looked quite promising with all the versions. This solution is generating WARNING.

    Warning: Attempt to present on whose view is not in the window hierarchy!

    https://stackoverflow.com/a/34487871/2369867 => This is looked promising then. But it was not in Swift 3. So I am answering this in Swift 3 and this is not template example.

    This is rather fully functional code by itself once you paste inside any function.

    Quick Swift 3 self-contained code

    let alertController = UIAlertController(title: "<your title>", message: "<your message>", preferredStyle: UIAlertControllerStyle.alert)
    alertController.addAction(UIAlertAction(title: "Close", style: UIAlertActionStyle.cancel, handler: nil))
    
    let alertWindow = UIWindow(frame: UIScreen.main.bounds)
    alertWindow.rootViewController = UIViewController()
    alertWindow.windowLevel = UIWindowLevelAlert + 1;
    alertWindow.makeKeyAndVisible()
    alertWindow.rootViewController?.present(alertController, animated: true, completion: nil)
    

    This is tested and working code in Swift 3.

    0 讨论(0)
  • 2020-11-22 06:52

    Zev Eisenberg's answer is simple and straightforward, but it does not always work, and it may fail with this warning message:

    Warning: Attempt to present <UIAlertController: 0x7fe6fd951e10>  
     on <ThisViewController: 0x7fe6fb409480> which is already presenting 
     <AnotherViewController: 0x7fe6fd109c00>
    

    This is because the windows rootViewController is not at the top of the presented views. To correct this we need to walk up the presentation chain, as shown in my UIAlertController extension code written in Swift 3:

       /// show the alert in a view controller if specified; otherwise show from window's root pree
    func show(inViewController: UIViewController?) {
        if let vc = inViewController {
            vc.present(self, animated: true, completion: nil)
        } else {
            // find the root, then walk up the chain
            var viewController = UIApplication.shared.keyWindow?.rootViewController
            var presentedVC = viewController?.presentedViewController
            while presentedVC != nil {
                viewController = presentedVC
                presentedVC = viewController?.presentedViewController
            }
            // now we present
            viewController?.present(self, animated: true, completion: nil)
        }
    }
    
    func show() {
        show(inViewController: nil)
    }
    

    Updates on 9/15/2017:

    Tested and confirmed that the above logic still works great in the newly available iOS 11 GM seed. The top voted method by agilityvision, however, does not: the alert view presented in a newly minted UIWindow is below the keyboard and potentially prevents the user from tapping its buttons. This is because in iOS 11 all windowLevels higher than that of keyboard window is lowered to a level below it.

    One artifact of presenting from keyWindow though is the animation of keyboard sliding down when alert is presented, and sliding up again when alert is dismissed. If you want the keyboard to stay there during presentation, you can try to present from the top window itself, as shown in below code:

    func show(inViewController: UIViewController?) {
        if let vc = inViewController {
            vc.present(self, animated: true, completion: nil)
        } else {
            // get a "solid" window with the highest level
            let alertWindow = UIApplication.shared.windows.filter { $0.tintColor != nil || $0.className() == "UIRemoteKeyboardWindow" }.sorted(by: { (w1, w2) -> Bool in
                return w1.windowLevel < w2.windowLevel
            }).last
            // save the top window's tint color
            let savedTintColor = alertWindow?.tintColor
            alertWindow?.tintColor = UIApplication.shared.keyWindow?.tintColor
    
            // walk up the presentation tree
            var viewController = alertWindow?.rootViewController
            while viewController?.presentedViewController != nil {
                viewController = viewController?.presentedViewController
            }
    
            viewController?.present(self, animated: true, completion: nil)
            // restore the top window's tint color
            if let tintColor = savedTintColor {
                alertWindow?.tintColor = tintColor
            }
        }
    }
    

    The only not so great part of the above code is that it checks the class name UIRemoteKeyboardWindow to make sure we can include it too. Nevertheless the above code does work great in iOS 9, 10 and 11 GM seed, with the right tint color and without the keyboard sliding artifacts.

    0 讨论(0)
  • 2020-11-22 06:52

    There 2 approaches that you can use:

    -Use UIAlertView or 'UIActionSheet' instead (not recommended, cause it deprecated in iOS 8 but it works now)

    -Somehow remember the last view controller which is presented. Here is example.

    @interface UIViewController (TopController)
    + (UIViewController *)topViewController;
    @end
    
    // implementation
    
    #import "UIViewController+TopController.h"
    #import <objc/runtime.h>
    
    static __weak UIViewController *_topViewController = nil;
    
    @implementation UIViewController (TopController)
    
    + (UIViewController *)topViewController {
        UIViewController *vc = _topViewController;
        while (vc.parentViewController) {
            vc = vc.parentViewController;
        }
        return vc;
    }
    
    + (void)load {
        [super load];
        [self swizzleSelector:@selector(viewDidAppear:) withSelector:@selector(myViewDidAppear:)];
        [self swizzleSelector:@selector(viewWillDisappear:) withSelector:@selector(myViewWillDisappear:)];
    }
    
    - (void)myViewDidAppear:(BOOL)animated {
        if (_topViewController == nil) {
            _topViewController = self;
        }
    
        [self myViewDidAppear:animated];
    }
    
    - (void)myViewWillDisappear:(BOOL)animated {
        if (_topViewController == self) {
            _topViewController = nil;
        }
    
        [self myViewWillDisappear:animated];
    }
    
    + (void)swizzleSelector:(SEL)sel1 withSelector:(SEL)sel2
    {
        Class class = [self class];
    
        Method originalMethod = class_getInstanceMethod(class, sel1);
        Method swizzledMethod = class_getInstanceMethod(class, sel2);
    
        BOOL didAddMethod = class_addMethod(class,
                                            sel1,
                                            method_getImplementation(swizzledMethod),
                                            method_getTypeEncoding(swizzledMethod));
    
        if (didAddMethod) {
            class_replaceMethod(class,
                                sel2,
                                method_getImplementation(originalMethod),
                                method_getTypeEncoding(originalMethod));
        } else {
            method_exchangeImplementations(originalMethod, swizzledMethod);
        }
    }
    
    @end 
    

    Usage:

    [[UIViewController topViewController] presentViewController:alertController ...];
    
    0 讨论(0)
提交回复
热议问题