ipad - switch - Concurrent UIAlertControllers
switch ios (9)
También estoy enfrentando algunos problemas con UIAlertController cuando se trata de presentarlo. En este momento, la única solución que puedo sugerir es presentar el controlador de alerta desde el controlador ViewContrller presentado más arriba, en su caso, o en el controlador rootViewController de la ventana.
UIViewController *presentingViewController = [[[UIApplication sharedApplication] delegate] window].rootViewController;
while(presentingViewController.presentedViewController != nil)
{
presentingViewController = presentingViewController.presentedViewController;
}
[presentingViewController presentViewController:alertView animated:YES completion:nil];
La advertencia que está recibiendo no se limita a UIAlertController. Un controlador de vista (rootViewController de la ventana en su caso) puede presentar solo un controlador de vista a la vez.
Estoy portando mi aplicación a iOS 8.0 y noto que UIAlertView está en desuso.
Así que cambié las cosas para usar un UIAlertController. Lo cual funciona en la mayoría de las circunstancias.
Excepto que, cuando se abre mi aplicación, realiza varias comprobaciones para informar al estado de varios estados ...
Por ejemplo ... "Advertencia, no ha configurado X y debe hacer Y antes de completar proyectos" y "Advertencia, está utilizando una versión beta y no confía en los resultados", etc. (¡estos son solo ejemplos!)
Bajo UIAlertView, yo diría que obtendría dos cuadros de alerta al mismo tiempo que el usuario tiene que tocar dos veces para descartar ambos ... pero ambos aparecen.
Bajo UIAlertController con el siguiente código para presentar una alerta ''general'', solo recibo un mensaje de alerta junto con un mensaje de consola:
Advertencia: Intente presentar UIAlertController: 0x13f667bb0 en TestViewController: 0x13f63cb40 que ya presenta UIAlertController: 0x13f54edf0
Entonces, aunque lo anterior probablemente no sea un buen ejemplo, creo que puede haber ocasiones en las que deba presentarse más de una alerta global debido a ''eventos'' mientras se opera una aplicación. Bajo el viejo UIAlertView, aparecerían pero parece que no lo estarán bajo un UIAlertController.
¿Alguien puede sugerir cómo se podría lograr esto con un UIAlertController?
Gracias
+(void)presentAlert:(NSString*)alertMessage withTitle:(NSString*)title
{
UIAlertController *alertView = [UIAlertController
alertControllerWithTitle:title
message:alertMessage
preferredStyle:UIAlertControllerStyleAlert];
UIAlertAction* ok = [UIAlertAction
actionWithTitle:kOkButtonTitle
style:UIAlertActionStyleDefault
handler:^(UIAlertAction * action)
{
//Do some thing here
[alertView dismissViewControllerAnimated:YES completion:nil];
}];
[alertView addAction:ok];
UIViewController *rootViewController = [[[UIApplication sharedApplication] delegate] window].rootViewController;
[rootViewController presentViewController:alertView animated:YES completion:nil];
Editar: Noté que en iOS8, presentando dos AlertViews consecutivamente, están ''en cola'' y aparecen secuencialmente, mientras que en iOS7 aparecen al mismo tiempo. Parece que Apple ha alterado UIAlertView para poner en cola varias instancias. ¿Hay alguna manera de hacer esto con UIAlertController sin seguir usando UIAlertView (obsoleto pero modificado) ???
Resolví este problema con esta línea de código:
alert.modalTransitionStyle=UIModalPresentationOverCurrentContext;
Esto puede resolverse usando un indicador de verificación en el controlador de acción de UIAlertcontroller.
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
_isShowAlertAgain = YES;
[self showAlert];
}
- (void)showAlert {
UIAlertController *alertController = [UIAlertController alertControllerWithTitle:@"Alert" message:@"This is Alert" preferredStyle:UIAlertControllerStyleAlert];
UIAlertAction *okButton = [UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) {
[alertController dismissViewControllerAnimated:YES completion:nil];
if (_isShowAlertAgain) {
_isShowAlertAgain = NO;
[self showAlert];
}
}];
UIAlertAction *cancelButton = [UIAlertAction actionWithTitle:@"Cancel" style:UIAlertActionStyleCancel handler:^(UIAlertAction *action) {
[alertController dismissViewControllerAnimated:YES completion:nil];
}];
[alertController addAction:okButton];
[alertController addAction:cancelButton];
[self presentViewController:alertController animated:YES completion:nil];
}
Entiendo completamente el problema aquí y se me ocurrió la siguiente solución a través de una categoría de UIAlertController. Está diseñado para que, si ya se está presentando una alerta, retrase la presentación de la siguiente alerta hasta que reciba una notificación que indique que la primera se ha descartado.
UIAlertController + MH.h
#import <UIKit/UIKit.h>
@interface UIAlertController (MH)
// Gives previous behavior of UIAlertView in that alerts are queued up.
-(void)mh_show;
@end
UIAlertController + MH.m
@implementation UIAlertController (MH)
// replace the implementation of viewDidDisappear via swizzling.
+ (void)load {
static dispatch_once_t once_token;
dispatch_once(&once_token, ^{
Method originalMethod = class_getInstanceMethod(self, @selector(viewDidDisappear:));
Method extendedMethod = class_getInstanceMethod(self, @selector(mh_viewDidDisappear:));
method_exchangeImplementations(originalMethod, extendedMethod);
});
}
-(UIWindow*)mh_alertWindow{
return objc_getAssociatedObject(self, "mh_alertWindow");
}
-(void)mh_setAlertWindow:(UIWindow*)window{
objc_setAssociatedObject(self, "mh_alertWindow", window, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
-(void)mh_show{
void (^showAlert)() = ^void() {
UIWindow* w = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
// we need to retain the window so it can be set to hidden before it is dealloced so the observation fires.
[self mh_setAlertWindow:w];
w.rootViewController = [[UIViewController alloc] init];
w.windowLevel = UIWindowLevelAlert;
[w makeKeyAndVisible];
[w.rootViewController presentViewController:self animated:YES completion:nil];
};
// check if existing key window is an alert already being shown. It could be our window or a UIAlertView''s window.
UIWindow* keyWindow = [UIApplication sharedApplication].keyWindow;
if(keyWindow.windowLevel == UIWindowLevelAlert){
// if it is, then delay showing this new alert until the previous has been dismissed.
__block id observer;
observer = [[NSNotificationCenter defaultCenter] addObserverForName:UIWindowDidBecomeHiddenNotification object:keyWindow queue:nil usingBlock:^(NSNotification * _Nonnull note) {
[[NSNotificationCenter defaultCenter] removeObserver:observer];
showAlert();
}];
}else{
// otherwise show the alert immediately.
showAlert();
}
}
- (void)mh_viewDidDisappear:(BOOL)animated {
[self mh_viewDidDisappear:animated]; // calls the original implementation
[self mh_alertWindow].hidden = YES;
}
@end
Este código incluso maneja el caso donde se presentó una alerta previa a través del UIAlertView en desuso, es decir, espera a que finalice también.
Para probar esto, todo lo que tiene que hacer es llamar al programa dos veces seguidas con dos controladores de alerta diferentes y verá que el segundo espera hasta que el primero se haya descartado antes de ser presentado.
He creado un proyecto Github MAAlertPresenter con una demostración para manejar este problema. Puede usarlo para presentar UIAlertController uno por uno con solo unas pocas líneas de cambios.
Esta solución está funcionando para mí. Tengo un AlertManager que maneja una cola de alertas que se presentan una tras otra. Para saber cuándo presentar otra alerta, extiendo UIAlertController y anulando su función viewDidDisappear.
Esta solución debe usarse después de viewDidAppear. Si no, la alerta no será presentada. La cadena estaría rota y no se presentarían más alertas. Otra opción sería probar una alerta colgante más tarde o descartarla, lo que liberaría la cola para futuras alertas.
/// This class presents one alert after another.
/// - Attention: If one of the alerts are not presented for some reason (ex. before viewDidAppear), it will not disappear either and the chain will be broken. No further alerts would be shown.
class AlertHandler {
private var alertQueue = [UIAlertController]()
private var alertInProcess: UIAlertController?
// singleton
static let sharedAlerts = AlertHandler()
private init() {}
func addToQueue(alert: UIAlertController) {
alertQueue.append(alert)
handleQueueAdditions()
}
private func handleQueueAdditions() {
if alertInProcess == nil {
let alert = alertQueue.removeFirst()
alertInProcess = alert
UIApplication.sharedApplication().keyWindow?.rootViewController?.presentViewController(alert, animated: true, completion: nil)
}
}
private func checkForNextAlert(alert: UIAlertController) {
if alert === alertInProcess {
if alertQueue.count > 0 {
let alert = alertQueue.removeFirst()
alertInProcess = alert
UIApplication.sharedApplication().keyWindow?.rootViewController?.presentViewController(alert, animated: true, completion: nil)
} else {
alertInProcess = nil
}
}
}
}
extension UIAlertController {
public override func viewDidDisappear(animated: Bool) {
AlertHandler.sharedAlerts.checkForNextAlert(self)
}
}
AlertHandler.sharedAlerts.addToQueue(alert:)
Parece ser una vieja pregunta, pero sigue publicando, ya que podría ser útil para alguien que busca esto, aunque Apple no recomienda múltiples alertas de apilamiento, por eso desaprobaron este UIAlertView de la implementación de UIAlertController.
Creé una subclase AQAlertAction para UIAlertAction. Puede usarlo para escalonar las alertas, el uso es el mismo que el que usa UIAlertAction. Todo lo que necesita hacer es importar AQMutiAlertFramework en su proyecto o puede incluir también clases (consulte el proyecto de ejemplo para eso). Internamente usa un semáforo binario para escalonar las alertas hasta que se muestre la acción de manejo del usuario asociada con la alerta actual. Avíseme si funciona para usted.
No estaba contento con ninguna de las soluciones aquí, ya que requerían demasiado trabajo manual o requerimiento de swizzling con el que no me siento cómodo en una aplicación de producción. Creé una nueva clase ( GitHub ) que toma elementos de otras respuestas aquí.
AlertQueue.h
#import <UIKit/UIKit.h>
@protocol AlertQueueItemDelegate;
@interface AlertQueueItem : NSObject
@property(nonatomic, weak, nullable) id<AlertQueueItemDelegate> delegate;
@property(nonatomic, readonly, nullable) NSDictionary * userInfo;
@end
@interface AlertQueue : NSObject
@property(nonatomic, readonly, nonnull) NSArray<AlertQueueItem *> *queuedAlerts;
@property(nonatomic, readonly, nullable) AlertQueueItem *displayedAlert;
+ (nonnull instancetype)sharedQueue;
- (nonnull AlertQueueItem *)displayAlert:(nonnull UIAlertController *)alert delegate:(nullable id<AlertQueueItemDelegate>)delegate userInfo:(nullable NSDictionary *)userInfo;
- (void)cancelAlert:(nonnull AlertQueueItem *)item;
@end
@protocol AlertQueueItemDelegate <NSObject>
- (void)alertDisplayed:(nonnull AlertQueueItem *)alertItem;
- (void)alertDismissed:(nonnull AlertQueueItem *)alertItem;
@end
AlertQueue.m
#import "AlertQueue.h"
@interface AlertQueue()
@property(nonatomic, strong, nonnull) NSMutableArray<AlertQueueItem *> *internalQueuedAlerts;
@property(nonatomic, strong, nullable) AlertQueueItem *displayedAlert;
@property(nonatomic, strong) UIWindow *window;
- (void)alertControllerDismissed:(nonnull UIAlertController *)alert;
@end
@interface AlertViewController : UIAlertController
@end
@implementation AlertViewController
- (void)viewDidDisappear:(BOOL)animated {
[super viewDidDisappear:animated];
[[AlertQueue sharedQueue] alertControllerDismissed:self];
}
@end
@interface AlertQueueItem()
@property(nonatomic, strong) AlertViewController *alert;
@property(nonatomic, strong, nullable) NSDictionary * userInfo;
@end
@implementation AlertQueueItem
@end
@implementation AlertQueue
+ (nonnull instancetype)sharedQueue {
static AlertQueue *sharedQueue = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
sharedQueue = [AlertQueue new];
});
return sharedQueue;
}
- (instancetype)init
{
self = [super init];
if (self) {
self.window = [UIWindow new];
self.window.windowLevel = UIWindowLevelAlert;
self.window.backgroundColor = nil;
self.window.opaque = NO;
self.window.rootViewController = [[UIViewController alloc] init];
self.window.rootViewController.view.backgroundColor = nil;
self.window.rootViewController.view.opaque = NO;
self.internalQueuedAlerts = [NSMutableArray arrayWithCapacity:1];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(windowDidBecomeHidden:) name:UIWindowDidBecomeHiddenNotification object:nil];
}
return self;
}
- (void)dealloc {
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
- (void)windowDidBecomeHidden:(NSNotification *)notification {
[self displayAlertIfPossible];
}
- (void)alertControllerDismissed:(UIAlertController *)alert {
if(alert != self.displayedAlert.alert) {
return;
}
AlertQueueItem *item = self.displayedAlert;
self.displayedAlert = nil;
[self.internalQueuedAlerts removeObjectAtIndex:0];
if([item.delegate respondsToSelector:@selector(alertDismissed:)]) {
[item.delegate alertDismissed:(AlertQueueItem * _Nonnull)item];
}
[self displayAlertIfPossible];
}
- (void)displayAlertIfPossible {
UIWindow *keyWindow = [UIApplication sharedApplication].keyWindow;
if(self.displayedAlert != nil || (keyWindow != self.window && keyWindow.windowLevel >= UIWindowLevelAlert)) {
return;
}
if(self.internalQueuedAlerts.count == 0) {
self.window.hidden = YES;
return;
}
self.displayedAlert = self.internalQueuedAlerts[0];
self.window.frame = [UIScreen mainScreen].bounds;
[self.window makeKeyAndVisible];
[self.window.rootViewController presentViewController:(AlertViewController * _Nonnull)self.displayedAlert.alert animated:YES completion:nil];
if([self.displayedAlert.delegate respondsToSelector:@selector(alertDisplayed:)]) {
[self.displayedAlert.delegate alertDisplayed:(AlertQueueItem * _Nonnull)self.displayedAlert];
}
}
- (AlertQueueItem *)displayAlert:(UIAlertController *)alert delegate:(id<AlertQueueItemDelegate>)delegate userInfo:(id)userInfo {
AlertQueueItem * item = [AlertQueueItem new];
item.alert = [AlertViewController alertControllerWithTitle:alert.title message:alert.message preferredStyle:alert.preferredStyle];
for(UIAlertAction *a in alert.actions) {
[item.alert addAction:a];
}
[self.internalQueuedAlerts addObject:item];
dispatch_async(dispatch_get_main_queue(), ^{
[self displayAlertIfPossible];
});
return item;
}
- (void)cancelAlert:(AlertQueueItem *)item {
if(item == self.displayedAlert) {
[self.displayedAlert.alert dismissViewControllerAnimated:YES completion:nil];
} else {
[self.internalQueuedAlerts removeObject:item];
}
}
- (NSArray<AlertQueueItem *> *)queuedAlerts {
return _internalQueuedAlerts;
}
@end
Ejemplo de uso
UIAlertController *ac = [UIAlertController alertControllerWithTitle:@"Test1" message:@"Test1" preferredStyle:UIAlertControllerStyleAlert];
[ac addAction:[UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleDefault handler:nil]];
AlertQueueItem *item = [[AlertQueue sharedQueue] displayAlert:ac delegate:nil userInfo:nil];
También me enfrenté al mismo problema después de cambiar de UIAlertView a UIAlertController. No me gusta la política de Apple porque "Message Boxes" siempre se han podido apilar en casi todos los SO de BIG BANG. Acepto que tener alertas simultáneas no es una gran experiencia de usuario y, a veces, es el resultado de un diseño incorrecto, pero a veces (por ejemplo, UILocalNotification o cosas por el estilo) pueden suceder y teme perder una alerta de bloqueo importante solo porque mi aplicación acaba de recibir una notificación.
Dicho esto, esta es mi solución 2cents, una función recursiva que intenta presentar el controlador de alertas en el remitente si el remitente no tiene presentado ViewController, de lo contrario, intenta presentar el controlador de alertas en el ViewViewController presentado y así sucesivamente ... No funciona si dispara más AlertController exactamente al mismo tiempo porque no puedes presentar un controlador de visualización desde un controlador que se está presentando pero debería funcionar en cualquier otro flujo de trabajo razonable.
+ (void)presentAlert:(UIAlertController *)alert withSender:(id)sender
{
if ([sender presentedViewController])
{
[self presentAlert:alert withSender: [sender presentedViewController]];
}
else
{
[sender presentViewController:alert animated:YES completion:nil];
}
}