¿Cómo presentar UIAlertController cuando no está en un controlador de vista?

Resuelto Murray Sagal asked hace 54 años • 39 respuestas

Escenario: el usuario toca un botón en un controlador de vista. El controlador de vista es el que se encuentra en la parte superior (obviamente) de la pila de navegación. El grifo invoca un método de clase de utilidad llamado en otra clase. Algo malo sucede allí y quiero mostrar una alerta allí mismo antes de que el control regrese al controlador de vista.

+ (void)myUtilityMethod {
    // do stuff
    // something bad happened, display an alert.
}

Esto fue posible con UIAlertView(pero quizás no del todo apropiado).

En este caso, ¿cómo se presenta un UIAlertController, justo ahí en myUtilityMethod?

Murray Sagal avatar Jan 01 '70 08:01 Murray Sagal
Aceptado

En la WWDC, me detuve en uno de los laboratorios y le hice la misma pregunta a un ingeniero de Apple: "¿Cuál fue la mejor práctica para mostrar un UIAlertController?" Y dijo que habían recibido muchas preguntas sobre esta cuestión y bromeamos diciendo que deberían haber tenido una sesión sobre ello. Dijo que internamente Apple está creando un UIWindowtransparente UIViewControllery luego presentandolo UIAlertController. Básicamente lo que hay en la respuesta de Dylan Betterman.

Pero no quería usar una subclase de UIAlertControllerporque eso requeriría que cambiara mi código en toda mi aplicación. Entonces, con la ayuda de un objeto asociado, creé una categoría UIAlertControllerque proporciona un showmétodo en Objective-C.

Aquí está el código relevante:

#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

Aquí hay un ejemplo de uso:

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

El UIWindowobjeto creado se destruirá cuando se UIAlertControllerdesasigne, ya que es el único objeto que conserva el archivo UIWindow. Pero si asigna el UIAlertControllera una propiedad o hace que su recuento de retención aumente al acceder a la alerta en uno de los bloques de acción, permanecerá UIWindowen la pantalla, bloqueando su interfaz de usuario. Consulte el código de uso de muestra anterior para evitarlo en caso de necesitar acceder UITextField.

Hice un repositorio de GitHub con un proyecto de prueba: FFGlobalAlertController

agilityvision avatar Jun 19 '2015 15:06 agilityvision

Rápido

let alertController = UIAlertController(title: "title", message: "message", preferredStyle: .alert)
//...
var rootViewController = UIApplication.shared.keyWindow?.rootViewController
if let navigationController = rootViewController as? UINavigationController {
    rootViewController = navigationController.viewControllers.first
}
if let tabBarController = rootViewController as? UITabBarController {
    rootViewController = tabBarController.selectedViewController
}
//...
rootViewController?.present(alertController, animated: true, completion: nil)

C objetivo

UIAlertController *alertController = [UIAlertController alertControllerWithTitle:@"Title" message:@"message" preferredStyle:UIAlertControllerStyleAlert];
//...
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:alertController animated:YES completion:nil];
Darkngs avatar Nov 04 '2014 13:11 Darkngs