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

255

Escenario: el usuario toca un botón en un controlador de vista. El controlador de vista es el más alto (obviamente) en la pila de navegación. El toque invoca un método de clase de utilidad llamado en otra clase. Algo malo sucede allí y quiero mostrar una alerta justo antes de que el control vuelva 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 presentas un UIAlertController, allí mismo myUtilityMethod?

Murray Sagal
fuente

Respuestas:

34

Publiqué una pregunta similar hace un par de meses y creo que finalmente he resuelto el problema. Sigue el enlace en la parte inferior de mi publicación si solo quieres ver el código.

La solución es usar una UIWindow adicional.

Cuando desee mostrar su UIAlertController:

  1. Haga que su ventana sea la clave y la ventana visible ( window.makeKeyAndVisible())
  2. Simplemente use una instancia simple de UIViewController como rootViewController de la nueva ventana. ( window.rootViewController = UIViewController())
  3. Presente su UIAlertController en el rootViewController de su ventana

Un par de cosas a tener en cuenta:

  • Su UIWindow debe estar fuertemente referenciada. Si no está fuertemente referenciado, nunca aparecerá (porque está liberado). Recomiendo usar una propiedad, pero también he tenido éxito con un objeto asociado .
  • Para garantizar que la ventana aparezca por encima de todo lo demás (incluido el sistema UIAlertControllers), configuro windowLevel. ( window.windowLevel = UIWindowLevelAlert + 1)

Por último, tengo una implementación completa si solo quieres ver eso.

https://github.com/dbettermann/DBAlertController

Dylan Bettermann
fuente
No tienes esto para Objective-C, ¿verdad?
SAHM
2
Sí, incluso funciona en Swift 2.0 / iOS 9. Estoy trabajando en una versión de Objective-C en este momento porque alguien más lo pidió (tal vez fuiste tú). Volveré a publicar cuando termine.
Dylan Bettermann
322

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

Pero no quería usar una subclase de UIAlertControllerporque eso requeriría cambiar mi código en toda mi aplicación. Entonces, con la ayuda de un objeto asociado, hice 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 UIWindowque se crea se destruirá cuando UIAlertControllerse desasigne, ya que es el único objeto que retiene el UIWindow. Pero si asigna la UIAlertControllerpropiedad 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, UIWindowpermanecerá en la pantalla, bloqueando su IU. Consulte el código de uso de muestra anterior para evitar en caso de que necesite acceder UITextField.

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

agilityvision
fuente
1
¡Buen material! Solo algunos antecedentes: utilicé una subclase en lugar de un objeto asociado porque estaba usando Swift. Los objetos asociados son una característica del tiempo de ejecución de Objective-C y no quería depender de él. Swift está probablemente a años de obtener su propio tiempo de ejecución, pero aún así. :)
Dylan Bettermann
1
Realmente me gusta la elegancia de su respuesta, sin embargo, tengo curiosidad por saber cómo retirar la nueva ventana y hacer que la ventana original vuelva a ser la clave (es cierto que no me gusta mucho la ventana).
Dustin Pfannenstiel
1
La ventana clave es la ventana visible más alta, por lo que entiendo que si quita / oculta la ventana "clave", la siguiente ventana visible hacia abajo se convierte en "clave".
agilityvision
19
Implementar viewDidDisappear:en una categoría parece una mala idea. En esencia, estás compitiendo con la implementación del marco de trabajo viewDidDisappear:. Por ahora puede estar bien, pero si Apple decide implementar ese método en el futuro, no hay forma de que lo llame (es decir, no hay ningún análogo de supereso apunta a la implementación primaria de un método desde una implementación de categoría) .
adib
55
Funciona muy bien, pero ¿cómo tratarlo prefersStatusBarHiddeny preferredStatusBarStylesin una subclase adicional?
Kevin Flachsmann
109

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
fuente
2
+1 Esta es una solución brillantemente simple. (Problema que enfrenté: Mostrar una alerta en la plantilla DetailViewController of Master / Detail - Muestra en iPad, nunca en iPhone)
David
8
Agradable, es posible que desee agregar otra parte: if (rootViewController.presentedViewController! = Nil) {rootViewController = rootViewController.presentedViewController; }
DivideByZer0
1
Swift 3: 'Alert' ha cambiado de nombre a 'alert': let alertController = UIAlertController (title: "title", message: "message", preferredStyle: .alert)
Kaptain el
¡Use un delegado en su lugar!
Andrew Kirna
104

Puede hacer lo siguiente con Swift 2.2:

let alertController: UIAlertController = ...
UIApplication.sharedApplication().keyWindow?.rootViewController?.presentViewController(alertController, animated: true, completion: nil)

Y Swift 3.0:

let alertController: UIAlertController = ...
UIApplication.shared.keyWindow?.rootViewController?.present(alertController, animated: true, completion: nil)
Zev Eisenberg
fuente
12
Vaya, acepté antes de comprobarlo. Ese código devuelve el controlador de vista raíz, que en mi caso es el controlador de navegación. No causa un error pero la alerta no se muestra.
Murray Sagal
22
Y me di cuenta en la consola: Warning: Attempt to present <UIAlertController: 0x145bfa30> on <UINavigationController: 0x1458e450> whose view is not in the window hierarchy!.
Murray Sagal
1
@MurraySagal con un controlador de navegación, puede obtener la visibleViewControllerpropiedad en cualquier momento para ver desde qué controlador presentar la alerta. Echa un vistazo a los documentos
Lubo
2
Lo hice porque no quiero tomar créditos del trabajo de otra persona. Fue la solución de @ZevEisenberg la que modifiqué para swift 3.0. Si hubiera agregado otra respuesta, entonces podría haber obtenido votos que se merece.
jeet.chanchawat
1
Oh, oye, me perdí todo el drama ayer, pero resulta que acabo de actualizar la publicación de Swift 3. No sé cuál es la política de SO para actualizar las respuestas antiguas para las nuevas versiones de idiomas, pero personalmente no me importa, ¡Mientras la respuesta sea correcta!
Zev Eisenberg
34

Bastante genérico UIAlertController extensionpara todos los casos de UINavigationControllery / o UITabBarController. También funciona si hay un VC modal en pantalla en este momento.

Uso:

//option 1:
myAlertController.show()
//option 2:
myAlertController.present(animated: true) {
    //completion code...
}

Esta es la extensión:

//Uses Swift1.2 syntax with the new if-let
// so it won't compile on a lower version.
extension UIAlertController {

    func show() {
        present(animated: true, completion: nil)
    }

    func present(#animated: Bool, completion: (() -> Void)?) {
        if let rootVC = UIApplication.sharedApplication().keyWindow?.rootViewController {
            presentFromController(rootVC, animated: animated, completion: completion)
        }
    }

    private func presentFromController(controller: UIViewController, animated: Bool, completion: (() -> Void)?) {
        if  let navVC = controller as? UINavigationController,
            let visibleVC = navVC.visibleViewController {
                presentFromController(visibleVC, animated: animated, completion: completion)
        } else {
          if  let tabVC = controller as? UITabBarController,
              let selectedVC = tabVC.selectedViewController {
                presentFromController(selectedVC, animated: animated, completion: completion)
          } else {
              controller.presentViewController(self, animated: animated, completion: completion)
          }
        }
    }
}
Aviel Gross
fuente
1
Estaba usando esta solución, y la encontré realmente perfecta, elegante, limpia ... PERO, recientemente tuve que cambiar mi controlador de vista raíz a una vista que no estaba en la jerarquía de vistas, por lo que este código se volvió inútil. ¿Alguien piensa en un dix por seguir usando esto?
1
Puedo utilizar una combinación de esta solución con sometinhg otra cosa: Tengo un singleton UIclase que tiene una (débil!) currentVCDe tipo UIViewController.I tienen BaseViewControllerque hereda de UIViewControllery conjunto UI.currentVCpara selfel viewDidAppearentonces nilsobre viewWillDisappear. Todos mis controladores de vista en la aplicación heredan BaseViewController. De esa manera, si tiene algo UI.currentVC(no es nil...), definitivamente no está en el medio de una animación de presentación, y puede pedirle que presente su UIAlertController.
Aviel Gross
1
Como se muestra a continuación, el controlador de vista raíz podría presentar algo con una segue, en cuyo caso su última declaración if falla, por lo que tuve que agregar else { if let presentedViewController = controller.presentedViewController { presentedViewController.presentViewController(self, animated: animated, completion: completion) } else { controller.presentViewController(self, animated: animated, completion: completion) } }
Niklas
27

Para mejorar la respuesta de agilityvision , deberá crear una ventana con un controlador de vista raíz transparente y presentar la vista de alerta desde allí.

Sin embargo , siempre que tenga una acción en su controlador de alertas, no necesita mantener una referencia a la ventana . Como paso final del bloque del controlador de acciones, solo necesita ocultar la ventana como parte de la tarea de limpieza. Al tener una referencia a la ventana en el bloque del controlador, esto crea una referencia circular temporal que se rompería una vez que se descarta el controlador de alerta.

UIWindow* window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
window.rootViewController = [UIViewController new];
window.windowLevel = UIWindowLevelAlert + 1;

UIAlertController* alertCtrl = [UIAlertController alertControllerWithTitle:... message:... preferredStyle:UIAlertControllerStyleAlert];

[alertCtrl addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"OK",@"Generic confirm") style:UIAlertActionStyleCancel handler:^(UIAlertAction * _Nonnull action) {
    ... // do your stuff

    // very important to hide the window afterwards.
    // this also keeps a reference to the window until the action is invoked.
    window.hidden = YES;
}]];

[window makeKeyAndVisible];
[window.rootViewController presentViewController:alertCtrl animated:YES completion:nil];
adib
fuente
Perfecto, exactamente el consejo que necesitaba para cerrar la ventana, gracias amigo
thibaut noah
25

La siguiente solución no funcionó aunque parecía bastante prometedora con todas las versiones. Esta solución está generando ADVERTENCIA .

Advertencia: ¡ Intente presentar en cuya vista no está en la jerarquía de la ventana!

https://stackoverflow.com/a/34487871/2369867 => Esto parece prometedor entonces. Pero era no en Swift 3. Así que estoy respondiendo esto en Swift 3 y este no es un ejemplo de plantilla.

Este es un código bastante funcional por sí solo una vez que pegue dentro de cualquier función.

Código rápido Swift 3 e independiente

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)

Esto está probado y funciona en Swift 3.

codificador mítico
fuente
1
Este código funcionó perfectamente para mí, en un contexto donde se estaba disparando un UIAlertController en el Delegado de aplicaciones con respecto a un problema de migración, antes de que se cargara cualquier controlador de vista raíz. Funcionó muy bien, sin advertencias.
Duncan Babbage
3
Solo un recordatorio: debe almacenar una fuerte referencia a su UIWindowo la ventana se abrirá y desaparecerá poco después de salir del alcance.
Sirenas
24

Aquí está la respuesta de mythicalcoder como extensión, probada y trabajando en Swift 4:

extension UIAlertController {

    func presentInOwnWindow(animated: Bool, completion: (() -> Void)?) {
        let alertWindow = UIWindow(frame: UIScreen.main.bounds)
        alertWindow.rootViewController = UIViewController()
        alertWindow.windowLevel = UIWindowLevelAlert + 1;
        alertWindow.makeKeyAndVisible()
        alertWindow.rootViewController?.present(self, animated: animated, completion: completion)
    }

}

Ejemplo de uso:

let alertController = UIAlertController(title: "<Alert Title>", message: "<Alert Message>", preferredStyle: .alert)
alertController.addAction(UIAlertAction(title: "Close", style: .cancel, handler: nil))
alertController.presentInOwnWindow(animated: true, completion: {
    print("completed")
})
bobbyrehm
fuente
¡Esto se puede usar incluso si sharedApplication no es accesible!
Alfi
20

Esto funciona en Swift para controladores de vista normales e incluso si hay un controlador de navegación en la pantalla:

let alert = UIAlertController(...)

let alertWindow = UIWindow(frame: UIScreen.main.bounds)
alertWindow.rootViewController = UIViewController()
alertWindow.windowLevel = UIWindowLevelAlert + 1;
alertWindow.makeKeyAndVisible()
alertWindow.rootViewController?.presentViewController(alert, animated: true, completion: nil)
William Entriken
fuente
1
Cuando descarto la alerta, UIWindowno responde. Algo que ver con lo windowLevelprobable. ¿Cómo puedo hacer que responda?
control deslizante
1
Parece que no se descartó una nueva ventana.
Igor Kulagin
Parece que la ventana no se elimina de la parte superior, por lo que debe eliminar la ventana una vez hecho.
soan saini
Establecer su alertWindowpara nilcuando haya terminado con él.
C6Silver
13

Agregando a la respuesta de Zev (y volviendo a Objective-C), podría encontrarse con una situación en la que su controlador de vista raíz presenta algún otro VC a través de una segue u otra cosa. Llamar a presentViewController en el VC raíz se encargará de esto:

[[UIApplication sharedApplication].keyWindow.rootViewController.presentedViewController presentViewController:alertController animated:YES completion:^{}];

Esto solucionó un problema que tenía cuando el VC raíz se había segmentado a otro VC, y en lugar de presentar el controlador de alerta, se emitió una advertencia como las que se informaron anteriormente:

Warning: Attempt to present <UIAlertController: 0x145bfa30> on <UINavigationController: 0x1458e450> whose view is not in the window hierarchy!

No lo he probado, pero esto también puede ser necesario si su VC raíz es un controlador de navegación.

Kevin Sliech
fuente
Hum Me encuentro con este problema en Swift, y no encuentro cómo traducir su código objc a swift, ¡la ayuda sería muy apreciada!
2
@Mayerz traducir Objective-C a Swift no debería ser un gran problema;) pero aquí estás:UIApplication.sharedApplication().keyWindow?.rootViewController?.presentedViewController?.presentViewController(controller, animated: true, completion: nil)
borchero
Gracias Olivier, tienes razón, es fácil, y lo traduje de esta manera, pero el problema estaba en otro lado. ¡Gracias de cualquier manera!
Attempting to load the view of a view controller while it is deallocating is not allowed and may result in undefined behavior (<UIAlertController: 0x15cd4afe0>)
Mojo66
2
Fui con el mismo enfoque, utilice el rootViewController.presentedViewControllersi no es nulo, el uso de otra manera rootViewController. Para una solución totalmente genérica, puede ser necesario recorrer la cadena de presentedViewControllers para llegar al topmostVC
Protongun
9

La respuesta de @ agilityvision se tradujo a Swift4 / iOS11. No he usado cadenas localizadas, pero puedes cambiar eso fácilmente:

import UIKit

/** An alert controller that can be called without a view controller.
 Creates a blank view controller and presents itself over that
 **/
class AlertPlusViewController: UIAlertController {

    private var alertWindow: UIWindow?

    override func viewDidLoad() {
        super.viewDidLoad()
    }

    override func viewDidDisappear(_ animated: Bool) {
        super.viewDidDisappear(animated)
        self.alertWindow?.isHidden = true
        alertWindow = nil
    }

    func show() {
        self.showAnimated(animated: true)
    }

    func showAnimated(animated _: Bool) {

        let blankViewController = UIViewController()
        blankViewController.view.backgroundColor = UIColor.clear

        let window = UIWindow(frame: UIScreen.main.bounds)
        window.rootViewController = blankViewController
        window.backgroundColor = UIColor.clear
        window.windowLevel = UIWindowLevelAlert + 1
        window.makeKeyAndVisible()
        self.alertWindow = window

        blankViewController.present(self, animated: true, completion: nil)
    }

    func presentOkayAlertWithTitle(title: String?, message: String?) {

        let alertController = AlertPlusViewController(title: title, message: message, preferredStyle: .alert)
        let okayAction = UIAlertAction(title: "Ok", style: .default, handler: nil)
        alertController.addAction(okayAction)
        alertController.show()
    }

    func presentOkayAlertWithError(error: NSError?) {
        let title = "Error"
        let message = error?.localizedDescription
        presentOkayAlertWithTitle(title: title, message: message)
    }
}
Dylan Colaco
fuente
Estaba obteniendo un fondo negro con la respuesta aceptada. window.backgroundColor = UIColor.cleararreglado eso. viewController.view.backgroundColor = UIColor.clearno parece ser necesario
Ben Patch
Tenga en cuenta que Apple advierte sobre UIAlertControllersubclases: The UIAlertController class is intended to be used as-is and does not support subclassing. The view hierarchy for this class is private and must not be modified. developer.apple.com/documentation/uikit/uialertcontroller
Grubas
6

Crear extensión como en la respuesta de Aviel Gross. Aquí tienes la extensión Objective-C.

Aquí tienes el archivo de encabezado * .h

//  UIAlertController+Showable.h

#import <UIKit/UIKit.h>

@interface UIAlertController (Showable)

- (void)show;

- (void)presentAnimated:(BOOL)animated
             completion:(void (^)(void))completion;

- (void)presentFromController:(UIViewController *)viewController
                     animated:(BOOL)animated
                   completion:(void (^)(void))completion;

@end

E implementación: * .m

//  UIAlertController+Showable.m

#import "UIAlertController+Showable.h"

@implementation UIAlertController (Showable)

- (void)show
{
    [self presentAnimated:YES completion:nil];
}

- (void)presentAnimated:(BOOL)animated
             completion:(void (^)(void))completion
{
    UIViewController *rootVC = [UIApplication sharedApplication].keyWindow.rootViewController;
    if (rootVC != nil) {
        [self presentFromController:rootVC animated:animated completion:completion];
    }
}

- (void)presentFromController:(UIViewController *)viewController
                     animated:(BOOL)animated
                   completion:(void (^)(void))completion
{

    if ([viewController isKindOfClass:[UINavigationController class]]) {
        UIViewController *visibleVC = ((UINavigationController *)viewController).visibleViewController;
        [self presentFromController:visibleVC animated:animated completion:completion];
    } else if ([viewController isKindOfClass:[UITabBarController class]]) {
        UIViewController *selectedVC = ((UITabBarController *)viewController).selectedViewController;
        [self presentFromController:selectedVC animated:animated completion:completion];
    } else {
        [viewController presentViewController:self animated:animated completion:completion];
    }
}

@end

Está utilizando esta extensión en su archivo de implementación de esta manera:

#import "UIAlertController+Showable.h"

UIAlertController* alert = [UIAlertController
    alertControllerWithTitle:@"Title here"
                     message:@"Detail message here"
              preferredStyle:UIAlertControllerStyleAlert];

UIAlertAction* defaultAction = [UIAlertAction
    actionWithTitle:@"OK"
              style:UIAlertActionStyleDefault
            handler:^(UIAlertAction * action) {}];
[alert addAction:defaultAction];

// Add more actions if needed

[alert show];
Marcin Kapusta
fuente
4

Cross post mi respuesta ya que estos dos hilos no están marcados como engañados ...

Ahora que UIViewControlleres parte de la cadena de respuesta, puede hacer algo como esto:

if let vc = self.nextResponder()?.targetForAction(#selector(UIViewController.presentViewController(_:animated:completion:)), withSender: self) as? UIViewController {

    let alert = UIAlertController(title: "A snappy title", message: "Something bad happened", preferredStyle: .Alert)
    alert.addAction(UIAlertAction(title: "OK", style: .Default, handler: nil))

    vc.presentViewController(alert, animated: true, completion: nil)
}
Mark Aufflick
fuente
4

La respuesta de Zev Eisenberg es simple y directa, pero no siempre funciona, y puede fallar con este mensaje de advertencia:

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

Esto se debe a que Windows rootViewController no está en la parte superior de las vistas presentadas. Para corregir esto, necesitamos avanzar por la cadena de presentación, como se muestra en mi código de extensión UIAlertController escrito en 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)
}

Actualizaciones el 15/09/2017:

Probado y confirmado que la lógica anterior todavía funciona muy bien en la semilla GM iOS 11 recién disponible. Sin embargo, el método más votado por agilityvision no lo hace: la vista de alerta presentada en una nueva acuñada UIWindowestá debajo del teclado y potencialmente evita que el usuario toque sus botones. Esto se debe a que en iOS 11 todos los niveles de ventana superiores a los de la ventana del teclado se reducen a un nivel inferior.

Sin embargo, un artefacto de presentación keyWindowes la animación del teclado deslizándose hacia abajo cuando se presenta la alerta, y deslizándose hacia arriba nuevamente cuando se desactiva la alerta. Si desea que el teclado permanezca allí durante la presentación, puede intentar presentar desde la ventana superior, como se muestra en el siguiente código:

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
        }
    }
}

La única parte no tan buena del código anterior es que verifica el nombre de la clase UIRemoteKeyboardWindowpara asegurarse de que podamos incluirlo también. Sin embargo, el código anterior funciona muy bien en iOS 9, 10 y 11 semillas GM, con el color de tinte correcto y sin los artefactos deslizantes del teclado.

CodeBrew
fuente
Acabo de leer las muchas respuestas anteriores aquí y vi la respuesta de Kevin Sliech, que está tratando de resolver el mismo problema con un enfoque similar, pero que no llegó a subir la cadena de presentación, lo que lo hace susceptible al mismo error que intenta resolver .
CodeBrew
4

Swift 4+

Solución que uso durante años sin ningún problema. En primer lugar, me extiendo UIWindowpara encontrar que es visibleViewController. NOTA : si usa clases de colección * personalizadas (como el menú lateral), debe agregar un controlador para este caso en la siguiente extensión. Después de obtener el mejor controlador de vista, es fácil presentarlo UIAlertControllercomo UIAlertView.

extension UIAlertController {

  func show(animated: Bool = true, completion: (() -> Void)? = nil) {
    if let visibleViewController = UIApplication.shared.keyWindow?.visibleViewController {
      visibleViewController.present(self, animated: animated, completion: completion)
    }
  }

}

extension UIWindow {

  var visibleViewController: UIViewController? {
    guard let rootViewController = rootViewController else {
      return nil
    }
    return visibleViewController(for: rootViewController)
  }

  private func visibleViewController(for controller: UIViewController) -> UIViewController {
    var nextOnStackViewController: UIViewController? = nil
    if let presented = controller.presentedViewController {
      nextOnStackViewController = presented
    } else if let navigationController = controller as? UINavigationController,
      let visible = navigationController.visibleViewController {
      nextOnStackViewController = visible
    } else if let tabBarController = controller as? UITabBarController,
      let visible = (tabBarController.selectedViewController ??
        tabBarController.presentedViewController) {
      nextOnStackViewController = visible
    }

    if let nextOnStackViewController = nextOnStackViewController {
      return visibleViewController(for: nextOnStackViewController)
    } else {
      return controller
    }
  }

}
Timur Bernikovich
fuente
4

Para iOS 13, basándose en las respuestas de mythicalcoder y bobbyrehm :

En iOS 13, si está creando su propia ventana para presentar la alerta, debe mantener una fuerte referencia a esa ventana o su alerta no se mostrará porque la ventana se desasignará inmediatamente cuando su referencia salga del alcance.

Además, deberá establecer la referencia en nulo nuevamente después de que se descarte la alerta para eliminar la ventana para continuar permitiendo la interacción del usuario en la ventana principal debajo de ella.

Puede crear una UIViewControllersubclase para encapsular la lógica de administración de memoria de la ventana:

class WindowAlertPresentationController: UIViewController {

    // MARK: - Properties

    private lazy var window: UIWindow? = UIWindow(frame: UIScreen.main.bounds)
    private let alert: UIAlertController

    // MARK: - Initialization

    init(alert: UIAlertController) {

        self.alert = alert
        super.init(nibName: nil, bundle: nil)
    }

    required init?(coder aDecoder: NSCoder) {

        fatalError("This initializer is not supported")
    }

    // MARK: - Presentation

    func present(animated: Bool, completion: (() -> Void)?) {

        window?.rootViewController = self
        window?.windowLevel = UIWindow.Level.alert + 1
        window?.makeKeyAndVisible()
        present(alert, animated: animated, completion: completion)
    }

    // MARK: - Overrides

    override func dismiss(animated flag: Bool, completion: (() -> Void)? = nil) {

        super.dismiss(animated: flag) {
            self.window = nil
            completion?()
        }
    }
}

Puede usar esto como está, o si desea un método conveniente para usted UIAlertController, puede incluirlo en una extensión:

extension UIAlertController {

    func presentInOwnWindow(animated: Bool, completion: (() -> Void)?) {

        let windowAlertPresentationController = WindowAlertPresentationController(alert: self)
        windowAlertPresentationController.present(animated: animated, completion: completion)
    }
}
Logan Gauthier
fuente
Esto no funciona si es necesario descartar la alerta de forma manual - el WindowAlertPresentationController es asignado de-Nunca, lo que resulta en una interfaz de usuario congelada - nada es interactivo debido a la ventana sigue ahí
JBlake
Si desea descartar la alerta manualmente, asegúrese de llamar dismissal WindowAlertPresentationController directamente alert.presentingViewController?.dismiss(animated: true, completion: nil)
JBlake
let alertController = UIAlertController (título: "título", mensaje: "mensaje", preferredStyle: .alert); alertController.presentInOwnWindow (animado: falso, finalización: nulo) ¡funciona muy bien para mí! ¡Gracias!
Brian
Esto funciona en iPhone 6 con iOS 12.4.5, pero no en iPhone 11 Pro con iOS 13.3.1. No hay error, pero la alerta nunca se muestra. Cualquier sugerencia sería apreciada.
jl303
Funciona muy bien para iOS 13. No funciona en Catalyst: una vez que se cierra la alerta, la aplicación no es interactiva. Vea la solución de @Peter Lapisu
JBlake
3

Forma abreviada de presentar la alerta en Objective-C:

[[[[UIApplication sharedApplication] keyWindow] rootViewController] presentViewController:alertController animated:YES completion:nil];

Donde alertControlleresta tuUIAlertController objeto?

NOTA: También deberá asegurarse de que su clase auxiliar se extienda UIViewController

ViperMav
fuente
3

Si alguien está interesado, creé una versión Swift 3 de @agilityvision answer. El código:

import Foundation
import UIKit

extension UIAlertController {

    var window: UIWindow? {
        get {
            return objc_getAssociatedObject(self, "window") as? UIWindow
        }
        set {
            objc_setAssociatedObject(self, "window", newValue, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC)
        }
    }

    open override func viewDidDisappear(_ animated: Bool) {
        super.viewDidDisappear(animated)
        self.window?.isHidden = true
        self.window = nil
    }

    func show(animated: Bool = true) {
        let window = UIWindow(frame: UIScreen.main.bounds)
        window.rootViewController = UIViewController(nibName: nil, bundle: nil)

        let delegate = UIApplication.shared.delegate
        if delegate?.window != nil {
            window.tintColor = delegate!.window!!.tintColor
        }

        window.windowLevel = UIApplication.shared.windows.last!.windowLevel + 1

        window.makeKeyAndVisible()
        window.rootViewController!.present(self, animated: animated, completion: nil)

        self.window = window
    }
}
Majster
fuente
@Chathuranga: he revertido tu edición. Ese "manejo de errores" es completamente innecesario.
Martin R
2
extension UIApplication {
    /// The top most view controller
    static var topMostViewController: UIViewController? {
        return UIApplication.shared.keyWindow?.rootViewController?.visibleViewController
    }
}

extension UIViewController {
    /// The visible view controller from a given view controller
    var visibleViewController: UIViewController? {
        if let navigationController = self as? UINavigationController {
            return navigationController.topViewController?.visibleViewController
        } else if let tabBarController = self as? UITabBarController {
            return tabBarController.selectedViewController?.visibleViewController
        } else if let presentedViewController = presentedViewController {
            return presentedViewController.visibleViewController
        } else {
            return self
        }
    }
}

Con esto, puede presentar fácilmente su alerta así

UIApplication.topMostViewController?.present(viewController, animated: true, completion: nil)

Una cosa a tener en cuenta es que si hay un UIAlertController que se muestra actualmente, UIApplication.topMostViewControllerdevolverá a UIAlertController. Presentar encima de un UIAlertControllertiene un comportamiento extraño y debe evitarse. Como tal, debe verificarlo manualmente !(UIApplication.topMostViewController is UIAlertController)antes de presentarlo o agregar un else ifcaso para devolver cero siself is UIAlertController

extension UIViewController {
    /// The visible view controller from a given view controller
    var visibleViewController: UIViewController? {
        if let navigationController = self as? UINavigationController {
            return navigationController.topViewController?.visibleViewController
        } else if let tabBarController = self as? UITabBarController {
            return tabBarController.selectedViewController?.visibleViewController
        } else if let presentedViewController = presentedViewController {
            return presentedViewController.visibleViewController
        } else if self is UIAlertController {
            return nil
        } else {
            return self
        }
    }
}
NSExceptional
fuente
1

Puede enviar la vista o controlador actual como parámetro:

+ (void)myUtilityMethod:(id)controller {
    // do stuff
    // something bad happened, display an alert.
}
Pablo A.
fuente
Sí, eso es posible y funcionaría. Pero para mí, tiene un poco de olor a código. Los parámetros pasados ​​generalmente deben ser necesarios para que el método llamado realice su función principal. Además, todas las llamadas existentes tendrían que modificarse.
Murray Sagal
1

Kevin Sliech proporcionó una gran solución.

Ahora uso el siguiente código en mi subclase principal UIViewController.

Una pequeña alteración que hice fue verificar si el mejor controlador de presentación no es un UIViewController simple. Si no, tiene que ser un VC que presente un VC simple. Por lo tanto, devolvemos el VC que se presenta en su lugar.

- (UIViewController *)bestPresentationController
{
    UIViewController *bestPresentationController = [UIApplication sharedApplication].keyWindow.rootViewController;

    if (![bestPresentationController isMemberOfClass:[UIViewController class]])
    {
        bestPresentationController = bestPresentationController.presentedViewController;
    }    

    return bestPresentationController;
}

Parece que todo ha funcionado hasta ahora en mis pruebas.

Gracias Kevin!

Andrés
fuente
1

Además de las excelentes respuestas dadas ( agilityvision , adib , malhal ). Para alcanzar el comportamiento de colas como en los viejos UIAlertViews (evite la superposición de ventanas de alerta), use este bloque para observar la disponibilidad a nivel de ventana:

@interface UIWindow (WLWindowLevel)

+ (void)notifyWindowLevelIsAvailable:(UIWindowLevel)level withBlock:(void (^)())block;

@end

@implementation UIWindow (WLWindowLevel)

+ (void)notifyWindowLevelIsAvailable:(UIWindowLevel)level withBlock:(void (^)())block {
    UIWindow *keyWindow = [UIApplication sharedApplication].keyWindow;
    if (keyWindow.windowLevel == level) {
        // window level is occupied, listen for windows to hide
        id observer;
        observer = [[NSNotificationCenter defaultCenter] addObserverForName:UIWindowDidBecomeHiddenNotification object:keyWindow queue:nil usingBlock:^(NSNotification *note) {
            [[NSNotificationCenter defaultCenter] removeObserver:observer];
            [self notifyWindowLevelIsAvailable:level withBlock:block]; // recursive retry
        }];

    } else {
        block(); // window level is available
    }
}

@end

Ejemplo completo:

[UIWindow notifyWindowLevelIsAvailable:UIWindowLevelAlert withBlock:^{
    UIWindow *alertWindow = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
    alertWindow.windowLevel = UIWindowLevelAlert;
    alertWindow.rootViewController = [UIViewController new];
    [alertWindow makeKeyAndVisible];

    UIAlertController *alertController = [UIAlertController alertControllerWithTitle:@"Alert" message:nil preferredStyle:UIAlertControllerStyleAlert];
    [alertController addAction:[UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleCancel handler:^(UIAlertAction *action) {
        alertWindow.hidden = YES;
    }]];

    [alertWindow.rootViewController presentViewController:alertController animated:YES completion:nil];
}];

Esto le permitirá evitar la superposición de ventanas de alerta. Se puede usar el mismo método para separar y poner en cola los controladores de vista para cualquier número de capas de ventana.

Roman B.
fuente
1

Intenté todo lo mencionado, pero sin éxito. El método que utilicé para Swift 3.0:

extension UIAlertController {
    func show() {
        present(animated: true, completion: nil)
    }

    func present(animated: Bool, completion: (() -> Void)?) {
        if var topController = UIApplication.shared.keyWindow?.rootViewController {
            while let presentedViewController = topController.presentedViewController {
                topController = presentedViewController
            }
            topController.present(self, animated: animated, completion: completion)
        }
    }
}
Dragisa Dragisic
fuente
1

Algunas de estas respuestas solo funcionaron en parte para mí, combinarlas en el siguiente método de clase en AppDelegate fue la solución para mí. Funciona en iPad, en vistas de UITabBarController, en UINavigationController, y al presentar modales. Probado en iOS 10 y 13.

+ (UIViewController *)rootViewController {
    UIViewController *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;
    if (rootViewController.presentedViewController != nil)
        rootViewController = rootViewController.presentedViewController;
    return rootViewController;
}

Uso:

[[AppDelegate rootViewController] presentViewController ...
Eerko
fuente
1

Soporte de escena iOS13 (cuando se usa UIWindowScene)

import UIKit

private var windows: [String:UIWindow] = [:]

extension UIWindowScene {
    static var focused: UIWindowScene? {
        return UIApplication.shared.connectedScenes
            .first { $0.activationState == .foregroundActive && $0 is UIWindowScene } as? UIWindowScene
    }
}

class StyledAlertController: UIAlertController {

    var wid: String?

    func present(animated: Bool, completion: (() -> Void)?) {

        //let window = UIWindow(frame: UIScreen.main.bounds)
        guard let window = UIWindowScene.focused.map(UIWindow.init(windowScene:)) else {
            return
        }
        window.rootViewController = UIViewController()
        window.windowLevel = .alert + 1
        window.makeKeyAndVisible()
        window.rootViewController!.present(self, animated: animated, completion: completion)

        wid = UUID().uuidString
        windows[wid!] = window
    }

    open override func viewDidDisappear(_ animated: Bool) {
        super.viewDidDisappear(animated)
        if let wid = wid {
            windows[wid] = nil
        }

    }

}
Peter Lapisu
fuente
UIAlerController no debe subclasificarse de acuerdo con la documentación developer.apple.com/documentation/uikit/uialertcontroller
acuerda
0

Puede intentar implementar una categoría UIViewControllercon mehtod como - (void)presentErrorMessage;Y y dentro de ese método implementa UIAlertController y luego presentarlo self. Que en su código de cliente tendrá algo como:

[myViewController presentErrorMessage];

De esa forma evitará parámetros innecesarios y advertencias sobre que la vista no está en la jerarquía de ventanas.

Vlad Soroka
fuente
Excepto que no tengo myViewControlleren el código donde sucede lo malo. Eso está en un método de utilidad que no sabe nada sobre el controlador de vista que lo llamó.
Murray Sagal
2
En mi humilde opinión, presentar cualquier vista (por lo tanto, alertas) al usuario es responsabilidad de ViewControllers. Entonces, si alguna parte del código no sabe nada sobre viewController, no debería presentar ningún error al usuario, sino pasarlo a partes del código "viewController aware"
Vlad Soroka
2
Estoy de acuerdo. Pero la conveniencia del ahora desaprobado UIAlertViewme llevó a romper esa regla en algunos puntos.
Murray Sagal
0

Hay 2 enfoques que puede usar:

-Utilizar UIAlertView o 'UIActionSheet' en su lugar (no recomendado, porque está en desuso en iOS 8 pero funciona ahora)

-De alguna manera recuerde el último controlador de vista que se presenta. Aquí hay un ejemplo.

@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 

Uso:

[[UIViewController topViewController] presentViewController:alertController ...];
Gralex
fuente
0

Uso este código con algunas pequeñas variaciones personales en mi clase AppDelegate

-(UIViewController*)presentingRootViewController
{
    UIViewController *vc = self.window.rootViewController;
    if ([vc isKindOfClass:[UINavigationController class]] ||
        [vc isKindOfClass:[UITabBarController class]])
    {
        // filter nav controller
        vc = [AppDelegate findChildThatIsNotNavController:vc];
        // filter tab controller
        if ([vc isKindOfClass:[UITabBarController class]]) {
            UITabBarController *tbc = ((UITabBarController*)vc);
            if ([tbc viewControllers].count > 0) {
                vc = [tbc viewControllers][tbc.selectedIndex];
                // filter nav controller again
                vc = [AppDelegate findChildThatIsNotNavController:vc];
            }
        }
    }
    return vc;
}
/**
 *   Private helper
 */
+(UIViewController*)findChildThatIsNotNavController:(UIViewController*)vc
{
    if ([vc isKindOfClass:[UINavigationController class]]) {
        if (((UINavigationController *)vc).viewControllers.count > 0) {
            vc = [((UINavigationController *)vc).viewControllers objectAtIndex:0];
        }
    }
    return vc;
}
Sound Blaster
fuente
0

Parece funcionar:

static UIViewController *viewControllerForView(UIView *view) {
    UIResponder *responder = view;
    do {
        responder = [responder nextResponder];
    }
    while (responder && ![responder isKindOfClass:[UIViewController class]]);
    return (UIViewController *)responder;
}

-(void)showActionSheet {
    UIAlertController *alertController = [UIAlertController alertControllerWithTitle:nil message:nil preferredStyle:UIAlertControllerStyleActionSheet];
    [alertController addAction:[UIAlertAction actionWithTitle:@"Cancel" style:UIAlertActionStyleCancel handler:nil]];
    [alertController addAction:[UIAlertAction actionWithTitle:@"Do it" style:UIAlertActionStyleDefault handler:nil]];
    [viewControllerForView(self) presentViewController:alertController animated:YES completion:nil];
}
wonder.mice
fuente
0

crear clase auxiliar AlertWindow y luego usar como

let alertWindow = AlertWindow();
let alert = UIAlertController(title: "Hello", message: "message", preferredStyle: .alert);
let cancel = UIAlertAction(title: "Ok", style: .cancel){(action) in

    //....  action code here

    // reference to alertWindow retain it. Every action must have this at end

    alertWindow.isHidden = true;

   //  here AlertWindow.deinit{  }

}
alert.addAction(cancel);
alertWindow.present(alert, animated: true, completion: nil)


class AlertWindow:UIWindow{

    convenience init(){
        self.init(frame:UIScreen.main.bounds);
    }

    override init(frame: CGRect) {
        super.init(frame: frame);
        if let color = UIApplication.shared.delegate?.window??.tintColor {
            tintColor = color;
        }
        rootViewController = UIViewController()
        windowLevel = UIWindowLevelAlert + 1;
        makeKeyAndVisible()
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    deinit{
        //  semaphor.signal();
    }

    func present(_ ctrl:UIViewController, animated:Bool, completion: (()->Void)?){
        rootViewController!.present(ctrl, animated: animated, completion: completion);
    }
}
john07
fuente