Come presentare UIAlertController quando non è in un controller di visualizzazione?


255

Scenario: l'utente tocca un pulsante su un controller di visualizzazione. Il controller di visualizzazione è il più in alto (ovviamente) nello stack di navigazione. Il rubinetto richiama un metodo della classe di utilità chiamato su un'altra classe. Accade qualcosa di brutto e voglio visualizzare un avviso proprio prima che il controllo ritorni al controller di visualizzazione.

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

Questo è stato possibile con UIAlertView(ma forse non del tutto corretto).

In questo caso, come si presenta un UIAlertController, proprio lì dentro myUtilityMethod?

Risposte:


34

Ho pubblicato una domanda simile un paio di mesi fa e penso di aver finalmente risolto il problema. Segui il link in fondo al mio post se vuoi solo vedere il codice.

La soluzione è utilizzare una UIWindow aggiuntiva.

Quando si desidera visualizzare UIAlertController:

  1. Rendi la tua finestra chiave e finestra visibile ( window.makeKeyAndVisible())
  2. Basta usare una semplice istanza di UIViewController come rootViewController della nuova finestra. ( window.rootViewController = UIViewController())
  3. Presenta il tuo UIAlertController sul rootViewController della tua finestra

Un paio di cose da notare:

  • La tua UIWindow deve essere fortemente referenziata. Se non è fortemente referenziato, non apparirà mai (perché è stato rilasciato). Consiglio di usare una proprietà, ma ho anche avuto successo con un oggetto associato .
  • Per garantire che la finestra appaia sopra ogni altra cosa (incluso il sistema UIAlertControllers), ho impostato windowLevel. ( window.windowLevel = UIWindowLevelAlert + 1)

Infine, ho un'implementazione completa se vuoi solo guardarla.

https://github.com/dbettermann/DBAlertController


Non hai questo per Objective-C, vero?
SAHM,

2
Sì, funziona anche in Swift 2.0 / iOS 9. Sto lavorando su una versione di Objective-C in questo momento perché qualcun altro l'ha chiesto (forse sei stato tu). Ti riporterò quando avrò finito.
Dylan Bettermann,

322

Al WWDC, mi sono fermato in uno dei laboratori e ho posto a un ingegnere Apple questa stessa domanda: "Qual è stata la migliore pratica per mostrare un UIAlertController?" E ha detto che stavano ricevendo molto questa domanda e abbiamo scherzato sul fatto che avrebbero dovuto avere una sessione su di essa. Ha detto che internamente Apple sta creando un UIWindowcon un trasparente UIViewControllere quindi presentandolo UIAlertController. Fondamentalmente cosa c'è nella risposta di Dylan Betterman.

Ma non volevo usare una sottoclasse di UIAlertControllerperché ciò mi avrebbe richiesto di cambiare il mio codice in tutta la mia app. Quindi, con l'aiuto di un oggetto associato, ho creato una categoria UIAlertControllerche fornisce un showmetodo in Objective-C.

Ecco il codice pertinente:

#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

Ecco un esempio di utilizzo:

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

Quello UIWindowche viene creato verrà distrutto quando UIAlertControllerviene distribuito, poiché è l'unico oggetto che mantiene UIWindow. Ma se si assegna la UIAlertControllerproprietà a una proprietà o si fa aumentare il suo conteggio di mantenimento accedendo all'avviso in uno dei blocchi di azione, UIWindowrimarrà sullo schermo, bloccando l'interfaccia utente. Vedere il codice di utilizzo di esempio sopra per evitare in caso di necessità di accesso UITextField.

Ho realizzato un repository GitHub con un progetto di test: FFGlobalAlertController


1
Roba buona! Solo uno sfondo: ho usato una sottoclasse anziché un oggetto associato perché stavo usando Swift. Gli oggetti associati sono una caratteristica del runtime di Objective-C e non volevo dipendere da esso. Swift è probabilmente a distanza di anni dall'ottenere il proprio tempo di esecuzione, ma comunque. :)
Dylan Bettermann il

1
Mi piace molto l'eleganza della tua risposta, tuttavia sono curioso di sapere come ritirare la nuova finestra e rendere di nuovo la finestra originale la chiave (è vero che non mi dilungo molto con la finestra).
Dustin Pfannenstiel,

1
La finestra chiave è la finestra più in alto visibile, quindi la mia comprensione è se si rimuove / nasconde la finestra "chiave", la successiva finestra visibile in basso diventa "chiave".
agilityvision,

19
L'implementazione viewDidDisappear:su una categoria sembra una cattiva idea. In sostanza, stai competendo con l'implementazione del framework di viewDidDisappear:. Per ora potrebbe andare bene, ma se Apple decide di implementare quel metodo in futuro, non c'è modo per te di chiamarlo (cioè non c'è analogo di superciò che indica l'implementazione primaria di un metodo da un'implementazione di categoria) .
adib,

5
Funziona alla grande, ma come trattare prefersStatusBarHiddene preferredStatusBarStylesenza una sottoclasse aggiuntiva?
Kevin Flachsmann,

109

veloce

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)

Objective-C

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

2
+1 Questa è una soluzione brillantemente semplice. (Problema che ho riscontrato: visualizzazione di un avviso nel modello DetailViewController of Master / Detail - Spettacoli su iPad, mai su iPhone)
David

8
Bene, potresti voler aggiungere un'altra parte: if (rootViewController.presentedViewController! = Nil) {rootViewController = rootViewController.presentedViewController; }
DivideByZer0

1
Swift 3: 'Alert' è stato rinominato in 'alert': let alertController = UIAlertController (titolo: "titolo", messaggio: "messaggio", stile preferito: .alert)
Kaptain,

Usa invece un delegato!
Andrew Kirna,

104

Puoi eseguire le seguenti operazioni con Swift 2.2:

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

E Swift 3.0:

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

12
Oops, ho accettato prima di controllare. Quel codice restituisce il controller della vista principale, che nel mio caso è il controller di navigazione. Non provoca un errore ma l'avviso non viene visualizzato.
Murray Sagal,

22
E ho notato nella console: Warning: Attempt to present <UIAlertController: 0x145bfa30> on <UINavigationController: 0x1458e450> whose view is not in the window hierarchy!.
Murray Sagal,

1
@MurraySagal con un controller di navigazione è possibile ottenere la visibleViewControllerproprietà in qualsiasi momento per vedere da quale controller presentare l'avviso. Consulta la documentazione
Lubo,

2
L'ho fatto perché non voglio prendermi il merito del lavoro di qualcun altro. È stata la soluzione di @ZevEisenberg che ho modificato per swift 3.0. Se avessi aggiunto un'altra risposta, avrei potuto ottenere i voti che merita.
jeet.chanchawat,

1
Oh hey, ieri mi sono perso tutto il dramma, ma mi è capitato di aver appena aggiornato il post per Swift 3. Non so quale sia la politica di SO sull'aggiornamento di vecchie risposte per le nuove versioni linguistiche, ma personalmente non mi dispiace, purché la risposta sia corretta!
Zev Eisenberg,

34

Abbastanza generico UIAlertController extensionper tutti i casi di UINavigationControllere / o UITabBarController. Funziona anche se al momento è presente un VC modale sullo schermo.

Uso:

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

Questa è l'estensione:

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

1
Stavo usando questa soluzione e l'ho trovata davvero perfetta, elegante, pulita ... MA recentemente ho dovuto cambiare il mio controller della vista radice in una vista non nella gerarchia della vista, quindi questo codice è diventato inutile. Qualcuno sta pensando a un DIX per continuare a usarlo?

1
Io uso una combinazione di questa soluzione con sometinhg altro: ho un Singleton UIclasse che detiene una (debole!) currentVCDi tipo UIViewController.Ho BaseViewControllerche eredita da UIViewControllere set UI.currentVCper selfil viewDidAppearpoi nilsu viewWillDisappear. Tutti i miei controller di visualizzazione nell'app ereditano BaseViewController. In questo modo se hai qualcosa UI.currentVC(non è nil...) - sicuramente non è nel mezzo di un'animazione di presentazione e puoi chiedergli di presentare il tuo UIAlertController.
Aviel Gross

1
Come sotto, il controller della vista radice potrebbe presentare qualcosa con un seguito, nel qual caso la tua ultima istruzione if fallisce, quindi ho dovuto aggiungere else { if let presentedViewController = controller.presentedViewController { presentedViewController.presentViewController(self, animated: animated, completion: completion) } else { controller.presentViewController(self, animated: animated, completion: completion) } }
Niklas

27

Migliorando la risposta di agilityvision , dovrai creare una finestra con un controller di visualizzazione radice trasparente e presentare la vista di avviso da lì.

Tuttavia , finché si dispone di un'azione nel controller di avviso, non è necessario mantenere un riferimento alla finestra . Come passaggio finale del blocco del gestore azioni, è sufficiente nascondere la finestra come parte dell'attività di pulizia. Avendo un riferimento alla finestra nel blocco del gestore, questo crea un riferimento circolare temporaneo che verrebbe interrotto una volta che il controller di avviso è stato eliminato.

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

Perfetto, esattamente la punta di cui avevo bisogno per chiudere la finestra, grazie
amico

25

La seguente soluzione non ha funzionato anche se sembrava abbastanza promettente con tutte le versioni. Questa soluzione sta generando AVVISO .

Avvertenza: tentare di presentare la cui vista non è nella gerarchia delle finestre!

https://stackoverflow.com/a/34487871/2369867 => Questo sembra allora promettente. Ma è stato , non a Swift 3. Quindi sto rispondendo a questo in Swift 3 e non lo è esempio di modello.

Questo è un codice piuttosto funzionale da solo una volta incollato all'interno di qualsiasi funzione.

Breve Swift 3 self-contained codice

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)

Questo è un codice testato e funzionante in Swift 3.


1
Questo codice ha funzionato perfettamente per me, in un contesto in cui un UIAlertController veniva licenziato nel delegato dell'app in merito a un problema di migrazione, prima che fosse caricato qualsiasi controller di visualizzazione principale. Ha funzionato alla grande, nessun avvertimento.
Duncan Babbage,

3
Solo un promemoria: è necessario memorizzare un riferimento forte al tuo UIWindowaltrimenti la finestra verrà rilasciata e scomparirà poco dopo essere uscita dall'ambito.
Sirene,

24

Ecco la risposta del mitico programmatore come estensione, testata e funzionante in 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)
    }

}

Esempio di utilizzo:

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

Questo può essere usato anche se sharedApplication non è accessibile!
Alfi,

20

Funziona in Swift per normali controller di visualizzazione e anche se sullo schermo è presente un controller di navigazione:

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)

1
Quando chiudo l'avviso, UIWindownon risponde. Qualcosa a che fare con il windowLevelprobabilmente. Come posso renderlo reattivo?
cursore

1
Sembra che la nuova finestra non sia stata ignorata.
Igor Kulagin,

Sembra che Window non venga rimosso dall'alto, quindi è necessario rimuovere la finestra una volta fatto.
soan saini,

Impostalo alertWindowsu nilquando hai finito.
C6 Argento

13

Aggiungendo alla risposta di Zev (e tornando a Objective-C), potresti imbatterti in una situazione in cui il tuo controller di root view presenta qualche altro VC tramite un seguito o qualcos'altro. La chiamata aViewViewController sul root VC si occuperà di questo:

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

Ciò ha risolto un problema che avevo riscontrato quando il VC di root era seguito a un altro VC e invece di presentare il controller di avviso, veniva emesso un avviso come quelli sopra riportati:

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

Non l'ho provato, ma questo potrebbe essere necessario anche se il tuo VC root è un controller di navigazione.


Hum, mi sto imbattendo in questo problema in Swift e non trovo come tradurre rapidamente il tuo codice objc, l'aiuto sarebbe molto apprezzato!

2
@Mayerz che traduce Objective-C in Swift non dovrebbe essere un grosso problema;) ma UIApplication.sharedApplication().keyWindow?.rootViewController?.presentedViewController?.presentViewController(controller, animated: true, completion: nil)
eccoti

Grazie Olivier, hai ragione, è facile come una torta, e l'ho tradotto in questo modo, ma il problema era da qualche altra parte. Grazie comunque!

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
Sono andato con lo stesso approccio, usare rootViewController.presentedViewControllerif se non zero, altrimenti usando rootViewController. Per una soluzione completamente generica, potrebbe essere necessario percorrere la catena di presentedViewControllers per arrivare al topmostVC
Protongun

9

La risposta di @ agilityvision tradotta in Swift4 / iOS11. Non ho usato stringhe localizzate, ma puoi cambiarlo facilmente:

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

Stavo ottenendo uno sfondo nero con la risposta accettata. window.backgroundColor = UIColor.clearrisolto quello. viewController.view.backgroundColor = UIColor.clearnon sembra essere necessario.
Ben Patch,

Tieni presente che Apple avverte della UIAlertControllersottoclasse: 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

Crea un'estensione come nella risposta Aviel Gross. Qui hai l'estensione Objective-C.

Qui hai il file di intestazione * .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 implementazione: * .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

Stai usando questa estensione nel tuo file di implementazione in questo modo:

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

4

Cross post la mia risposta poiché questi due thread non sono contrassegnati come duplicati ...

Ora che UIViewControllerfa parte della catena di responder, puoi fare qualcosa del genere:

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

4

La risposta di Zev Eisenberg è semplice e diretta, ma non sempre funziona e potrebbe non riuscire con questo messaggio di avviso:

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

Questo perché rootViewController di Windows non si trova nella parte superiore delle viste presentate. Per correggere questo, dobbiamo camminare lungo la catena di presentazione, come mostrato nel mio codice di estensione UIAlertController scritto 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)
}

Aggiornamenti il ​​15/09/2017:

Testato e confermato che la logica di cui sopra funziona ancora alla grande nel seme iOS 11 GM appena disponibile. Il metodo più votato da agilityvision, tuttavia, non lo fa: la vista di allerta presentata in una nuova coniataUIWindow è sotto la tastiera e potenzialmente impedisce all'utente di toccare i suoi pulsanti. Questo perché in iOS 11 tutti i livelli delle finestre superiori a quelli della finestra della tastiera vengono abbassati a un livello inferiore.

Un artefatto della presentazione da keyWindowsebbene è l'animazione della tastiera che scorre verso il basso quando viene presentato l'avviso e che si sposta di nuovo verso l'alto quando viene disattivato l'avviso. Se vuoi che la tastiera rimanga lì durante la presentazione, puoi provare a presentare dalla finestra superiore stessa, come mostrato nel codice seguente:

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

L'unica parte non eccezionale del codice sopra è che controlla il nome della classe UIRemoteKeyboardWindow per assicurarsi che possiamo includerlo anche. Tuttavia, il codice sopra funziona alla perfezione con i semi GM di iOS 9, 10 e 11, con il giusto colore e senza gli artefatti scorrevoli della tastiera.


Ho appena esaminato le molte risposte precedenti qui e ho visto la risposta di Kevin Sliech, che sta cercando di risolvere lo stesso problema con un approccio simile ma che ha smesso di camminare lungo la catena di presentazione, rendendolo quindi suscettibile allo stesso errore che tenta di risolvere .
CodeBrew

4

Swift 4+

Soluzione che uso da anni senza problemi. Prima di tutto mi estendo UIWindowper trovare visibleViewController. NOTA : se si utilizzano classi di raccolta personalizzate * (come il menu laterale), è necessario aggiungere il gestore per questo caso nella seguente estensione. Dopo aver ottenuto la maggior parte dei controller di visualizzazione, è facile presentarsi UIAlertControllerproprio come 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
    }
  }

}

4

Per iOS 13, sulla base delle risposte da mythicalcoder e bobbyrehm :

In iOS 13, se stai creando la tua finestra per presentare l'avviso, devi tenere un forte riferimento a quella finestra altrimenti il ​​tuo avviso non verrà visualizzato perché la finestra verrà immediatamente dislocata quando il suo riferimento esce dall'ambito.

Inoltre, dovrai impostare nuovamente il riferimento su zero dopo che l'allerta è stata eliminata per rimuovere la finestra per continuare a consentire l'interazione dell'utente nella finestra principale sotto di essa.

È possibile creare una UIViewControllersottoclasse per incapsulare la logica di gestione della memoria della finestra:

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?()
        }
    }
}

Puoi usarlo così com'è, o se vuoi un metodo di praticità sul tuo UIAlertController, puoi lanciarlo in un'estensione:

extension UIAlertController {

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

        let windowAlertPresentationController = WindowAlertPresentationController(alert: self)
        windowAlertPresentationController.present(animated: animated, completion: completion)
    }
}

Questo non funziona se è necessario chiudere manualmente l'avviso - WindowAlertPresentationController non viene mai disallocato, con conseguente interfaccia utente bloccata - nulla è interattivo a causa della finestra ancora lì
JBlake

Se vuoi chiudere manualmente l'avviso, assicurati di chiamare dismissdirettamente alert.presentingViewController?.dismiss(animated: true, completion: nil)
WindowAlertPresentationController

let alertController = UIAlertController (titolo: "titolo", messaggio: "messaggio", preferitoStyle: .alert); alertController.presentInOwnWindow (animato: falso, completamento: zero) funziona benissimo per me! Grazie!
Brian,

Funziona su iPhone 6 con iOS 12.4.5, ma non su iPhone 11 Pro con iOS 13.3.1. Non ci sono errori, ma l'avviso non viene mai visualizzato. Qualsiasi suggerimento sarebbe apprezzato.
jl303

Funziona benissimo per iOS 13. Non funziona in Catalyst: una volta che l'avviso viene eliminato, l'app non è in grado di interagire. Vedi la soluzione di
@Peter Lapisu

3

Modo abbreviato per presentare l'avviso in Objective-C:

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

Dov'è il alertControllertuoUIAlertController oggetto.

NOTA: è inoltre necessario assicurarsi che la classe di supporto si estenda UIViewController


3

Se qualcuno è interessato ho creato una versione Swift 3 della risposta @agilityvision. Il codice:

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

@Chathuranga: ho ripristinato la tua modifica. Quella "gestione degli errori" non è assolutamente necessaria.
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 questo puoi facilmente presentare il tuo avviso in questo modo

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

Una cosa da notare è che se è attualmente visualizzato un UIAlertController, UIApplication.topMostViewControllerverrà restituito a UIAlertController. Presentarsi sopra UIAlertControllerha un comportamento strano e dovrebbe essere evitato. Pertanto, è necessario verificarlo manualmente !(UIApplication.topMostViewController is UIAlertController)prima di presentare o aggiungere un else ifcaso per restituire zero seself 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
        }
    }
}

1

È possibile inviare la vista o il controller corrente come parametro:

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

Sì, è possibile e funzionerebbe. Ma per me, ha un po 'di odore di codice. I parametri passati dovrebbero generalmente essere richiesti affinché il metodo chiamato esegua la sua funzione primaria. Inoltre, tutte le chiamate esistenti dovrebbero essere modificate.
Murray Sagal,

1

Kevin Sliech ha fornito un'ottima soluzione.

Ora uso il codice seguente nella mia sottoclasse principale di UIViewController.

Una piccola modifica che ho fatto è stata quella di verificare se il miglior controller di presentazione non è un semplice UIViewController. In caso contrario, deve essere un VC che presenta un VC semplice. Quindi restituiamo il VC che viene presentato invece.

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

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

    return bestPresentationController;
}

Sembra tutto risolto finora nei miei test.

Grazie Kevin!


1

Oltre alle grandi risposte fornite ( agilityvision , adib , malhal ). Per raggiungere il comportamento in coda come nelle vecchie UIAlertViews (evitare sovrapposizioni di finestre di avviso), utilizzare questo blocco per osservare la disponibilità a livello di finestra:

@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

Esempio 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];
}];

Ciò consentirà di evitare la sovrapposizione di finestre di avviso. Lo stesso metodo può essere utilizzato per separare e inserire i controller di visualizzazione coda per qualsiasi numero di livelli finestra.


1

Ho provato tutto quanto menzionato, ma senza successo. Il metodo che ho usato per 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)
        }
    }
}

1

Alcune di queste risposte hanno funzionato solo in parte per me, combinandole nel seguente metodo di classe in AppDelegate è stata la soluzione per me. Funziona su iPad, nelle viste UITabBarController, in UINavigationController, quando presenta modali. Testato su iOS 10 e 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 ...

1

Supporto scena iOS13 (quando si utilizza 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
        }

    }

}

UIAlerController non dovrebbe essere sottoclassato secondo la documentazione developer.apple.com/documentation/uikit/uialertcontroller
accfews

0

Puoi provare a implementare una categoria UIViewControllercon mehtod come - (void)presentErrorMessage;And e all'interno di quel metodo implementi UIAlertController e poi presentalo self. Rispetto al tuo codice client avrai qualcosa del tipo:

[myViewController presentErrorMessage];

In questo modo eviterai parametri e avvertenze inutili sulla vista che non si trova nella gerarchia delle finestre.


Solo che non ho myViewControllernel codice in cui succede la cosa cattiva. Questo è in un metodo di utilità che non sa nulla del controller di visualizzazione che lo ha chiamato.
Murray Sagal

2
L'IMHO che presenta qualsiasi vista (quindi avvisi) all'utente è responsabilità di ViewController. Quindi, se una parte del codice non sa nulla di viewController, non dovrebbe presentare alcun errore all'utente, ma piuttosto passarlo a parti del codice "viewController aware"
Vlad Soroka,

2
Sono d'accordo. Ma la comodità dell'ormai deprecato UIAlertViewmi ha portato a infrangere quella regola in alcuni punti.
Murray Sagal,

0

Ci sono 2 approcci che puoi usare:

-Uso UIAlertView o 'UIActionSheet' invece (non raccomandato, perchè è deprecato in iOS 8 ma funziona ora)

-In qualche modo ricorda l'ultimo controller di visualizzazione che viene presentato. Ecco un esempio

@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 ...];

0

Uso questo codice con alcune piccole variazioni personali nella mia classe 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;
}

0

Sembra funzionare:

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

0

creare la classe helper AlertWindow e utilizzare come

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);
    }
}
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.