¿Cómo presentar UIAlertController cuando no está en un controlador de vista?
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
?
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 UIWindow
transparente UIViewController
y luego presentandolo UIAlertController
. Básicamente lo que hay en la respuesta de Dylan Betterman.
Pero no quería usar una subclase de UIAlertController
porque 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 UIAlertController
que proporciona un show
mé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 UIWindow
objeto creado se destruirá cuando se UIAlertController
desasigne, ya que es el único objeto que conserva el archivo UIWindow
. Pero si asigna el UIAlertController
a 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á UIWindow
en 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
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];