Comment présenter UIAlertController lorsqu'il n'est pas dans un contrôleur de vue?


255

Scénario: l'utilisateur appuie sur un bouton d'un contrôleur de vue. Le contrôleur de vue est le plus haut (évidemment) de la pile de navigation. Le tap appelle une méthode de classe utilitaire appelée sur une autre classe. Une mauvaise chose s'y produit et je veux afficher une alerte juste avant que le contrôle ne revienne au contrôleur de vue.

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

Cela a été possible avec UIAlertView (mais peut-être pas tout à fait approprié).

Dans ce cas, comment présentez-vous un UIAlertController, juste là myUtilityMethod?

Réponses:


34

J'ai posté une question similaire il y a quelques mois et je pense avoir finalement résolu le problème. Suivez le lien en bas de mon message si vous voulez juste voir le code.

La solution consiste à utiliser une fenêtre d'interface utilisateur supplémentaire.

Lorsque vous souhaitez afficher votre UIAlertController:

  1. Faites de votre fenêtre la clé et la fenêtre visible ( window.makeKeyAndVisible())
  2. Utilisez simplement une simple instance UIViewController comme rootViewController de la nouvelle fenêtre. ( window.rootViewController = UIViewController())
  3. Présentez votre UIAlertController sur rootViewController de votre fenêtre

Quelques points à noter:

  • Votre UIWindow doit être fortement référencé. S'il n'est pas référencé fortement, il n'apparaîtra jamais (car il est publié). Je recommande d'utiliser une propriété, mais j'ai également réussi avec un objet associé .
  • Pour m'assurer que la fenêtre apparaît au-dessus de tout le reste (y compris le système UIAlertControllers), j'ai défini le windowLevel. ( window.windowLevel = UIWindowLevelAlert + 1)

Enfin, j'ai une implémentation terminée si vous voulez juste regarder cela.

https://github.com/dbettermann/DBAlertController


Vous n'avez pas cela pour Objective-C, n'est-ce pas?
SAHM

2
Oui, cela fonctionne même dans Swift 2.0 / iOS 9. Je travaille actuellement sur une version Objective-C parce que quelqu'un d'autre l'a demandée (peut-être que c'était vous). Je posterai quand j'aurai fini.
Dylan Bettermann

322

À la WWDC, je me suis arrêté dans l'un des laboratoires et j'ai posé la même question à un ingénieur Apple: "Quelle était la meilleure pratique pour afficher un UIAlertController?" Et il a dit qu'ils avaient beaucoup posé cette question et nous avons plaisanté en disant qu'ils auraient dû avoir une séance à ce sujet. Il a dit qu'en interne, Apple crée un UIWindowavec un transparent UIViewController, puis présente leUIAlertController le dessus. Fondamentalement, ce qui est dans la réponse de Dylan Betterman.

Mais je ne voulais pas utiliser une sous-classe de UIAlertControllercar cela nécessiterait que je change mon code dans toute mon application. Donc, avec l'aide d'un objet associé, j'ai créé une catégorie UIAlertControllerqui fournit unshow méthode dans Objective-C.

Voici le code pertinent:

#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

Voici un exemple d'utilisation:

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

Le UIWindowqui est créé sera détruit lors de la UIAlertControllerdésallocation, car c'est le seul objet qui conserve le UIWindow. Mais si vous affectez le UIAlertControllerà une propriété ou que son nombre de retenues augmente en accédant à l'alerte dans l'un des blocs d'action, le UIWindowreste à l'écran, verrouillant votre interface utilisateur. Voir l'exemple de code d'utilisation ci-dessus pour éviter en cas de besoin d'accéderUITextField .

J'ai fait un dépôt GitHub avec un projet de test: FFGlobalAlertController


1
Bon produit! Juste un peu d'histoire - j'ai utilisé une sous-classe au lieu d'un objet associé parce que j'utilisais Swift. Les objets associés sont une fonctionnalité du runtime Objective-C et je ne voulais pas en dépendre. Swift est probablement à des années de son propre runtime, mais quand même. :)
Dylan Bettermann

1
J'aime vraiment l'élégance de votre réponse, mais je suis curieux de savoir comment vous retirez la nouvelle fenêtre et faites de la fenêtre d'origine la clé à nouveau (certes, je ne mouille pas beaucoup avec la fenêtre).
Dustin Pfannenstiel

1
La fenêtre clé est la fenêtre visible la plus haute, donc si je comprends bien, si vous supprimez / masquez la fenêtre "clé", la fenêtre visible suivante devient "clé".
agilityvision

19
La mise viewDidDisappear:en œuvre sur une catégorie ressemble à une mauvaise idée. En substance, vous êtes en concurrence avec la mise en œuvre du cadre de viewDidDisappear:. Pour l'instant, cela peut être correct, mais si Apple décide de mettre en œuvre cette méthode à l'avenir, il n'y a aucun moyen pour vous de l'appeler (c'est-à-dire qu'il n'y a pas d'analogue de supercela qui pointe vers la mise en œuvre principale d'une méthode à partir d'une mise en œuvre de catégorie) .
adib

5
Fonctionne très bien, mais comment traiter prefersStatusBarHiddenet preferredStatusBarStylesans sous-classe supplémentaire?
Kevin Flachsmann

109

Rapide

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)

Objectif 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 Il s'agit d'une solution brillamment simple. (Problème que j'ai rencontré: Affichage d'une alerte dans le modèle DetailViewController de Master / Detail - Affiche sur iPad, jamais sur iPhone)
David

8
Bien, vous voudrez peut-être ajouter une autre partie: if (rootViewController.presentedViewController! = Nil) {rootViewController = rootViewController.presentedViewController; }
DivideByZer0

1
Swift 3: 'Alert' a été renommé en 'alert': let alertController = UIAlertController (title: "title", message: "message", preferStyle: .alert)
Kaptain

Utilisez plutôt un délégué!
Andrew Kirna

104

Vous pouvez effectuer les opérations suivantes avec Swift 2.2:

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

Et Swift 3.0:

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

12
Oups, j'ai accepté avant de vérifier. Ce code renvoie le contrôleur de vue racine, qui dans mon cas est le contrôleur de navigation. Cela ne provoque pas d'erreur mais l'alerte ne s'affiche pas.
Murray Sagal

22
Et j'ai remarqué dans la console: Warning: Attempt to present <UIAlertController: 0x145bfa30> on <UINavigationController: 0x1458e450> whose view is not in the window hierarchy!.
Murray Sagal

1
@MurraySagal ayant un contrôleur de navigation, vous pouvez obtenir la visibleViewControllerpropriété à tout moment pour voir de quel contrôleur présenter l'alerte. Découvrez les documents
Lubo

2
Je l'ai fait parce que je ne veux pas prendre le crédit du travail de quelqu'un d'autre. C'était la solution de @ZevEisenberg que j'ai modifiée pour swift 3.0. Si j'aurais ajouté une autre réponse, j'aurais peut-être obtenu des votes qu'il mérite.
jeet.chanchawat

1
Oh hé, j'ai raté tout le drame hier, mais il se trouve que je viens de mettre à jour le message pour Swift 3. Je ne sais pas quelle est la politique de SO sur la mise à jour des anciennes réponses pour les nouvelles versions linguistiques, mais personnellement, cela ne me dérange pas, tant que la réponse est correcte!
Zev Eisenberg

34

Assez générique UIAlertController extensionpour tous les cas de UINavigationControlleret / ou UITabBarController. Fonctionne également s'il y a un VC modal à l'écran pour le moment.

Usage:

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

Ceci est l'extension:

//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
J'utilisais cette solution, et je l'ai trouvée vraiment parfaite, élégante, propre ... MAIS, récemment, j'ai dû changer mon contrôleur de vue racine pour une vue qui ne se trouve pas dans la hiérarchie des vues, donc ce code est devenu inutile. Quelqu'un pense à un dix pour continuer à utiliser ça?

1
J'utilise une combinaison de cette solution avec autre chose: j'ai une UIclasse singleton qui contient un (faible!) De currentVCtype. UIViewControllerJ'ai BaseViewControllerqui hérite de UIViewControlleret défini UI.currentVCsur selfon viewDidAppearpuis sur nilon viewWillDisappear. Tous mes contrôleurs de vue dans l'application héritent BaseViewController. De cette façon, si vous avez quelque chose UI.currentVC(ce n'est pas nil...) - ce n'est certainement pas au milieu d'une animation de présentation, et vous pouvez lui demander de présenter votre UIAlertController.
Aviel Gross

1
Comme ci-dessous, le contrôleur de vue racine peut présenter quelque chose avec une séquence, auquel cas votre dernière instruction if échoue, j'ai donc dû ajouter else { if let presentedViewController = controller.presentedViewController { presentedViewController.presentViewController(self, animated: animated, completion: completion) } else { controller.presentViewController(self, animated: animated, completion: completion) } }
Niklas

27

Pour améliorer la réponse d'agilityvision , vous devrez créer une fenêtre avec un contrôleur de vue racine transparent et présenter la vue d'alerte à partir de là.

Cependant, tant que vous avez une action dans votre contrôleur d'alerte, vous n'avez pas besoin de conserver une référence à la fenêtre . Comme étape finale du bloc du gestionnaire d'actions, il vous suffit de masquer la fenêtre dans le cadre de la tâche de nettoyage. En ayant une référence à la fenêtre dans le bloc de gestionnaire, cela crée une référence circulaire temporaire qui serait rompue une fois le contrôleur d'alerte fermé.

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

Parfait, exactement le conseil dont j'avais besoin pour fermer la fenêtre, merci mate
thibaut noah

25

La solution suivante n'a pas fonctionné même si elle semblait assez prometteuse avec toutes les versions. Cette solution génère AVERTISSEMENT .

Attention: Tentez de présenter sur la vue de laquelle ne figure pas dans la hiérarchie des fenêtres!

https://stackoverflow.com/a/34487871/2369867 => Cela semble alors prometteur. Mais il était pas dans Swift 3. Je réponds donc à cela dans Swift 3 et ce n'est pas exemple de modèle.

Il s'agit d'un code plutôt entièrement fonctionnel en soi une fois que vous avez collé à l'intérieur d'une fonction.

Code Swift 3 autonome rapide

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)

Ceci est testé et fonctionne dans Swift 3.


1
Ce code a parfaitement fonctionné pour moi, dans un contexte où un UIAlertController était déclenché dans le délégué d'application concernant un problème de migration, avant le chargement de tout contrôleur de vue racine. Fonctionne très bien, aucun avertissement.
Duncan Babbage

3
Juste un rappel: vous devez stocker une référence forte à votre UIWindowou bien la fenêtre sera libérée et disparaîtra peu de temps après être sortie du champ d'application.
Sirens

24

Voici la réponse de mythicalcoder en tant qu'extension, testée et fonctionnant dans 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)
    }

}

Exemple d'utilisation:

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

Cela peut être utilisé même si sharedApplication n'est pas accessible!
Alfi

20

Cela fonctionne dans Swift pour les contrôleurs de vue normaux et même s'il y a un contrôleur de navigation à l'écran:

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
Lorsque je rejette l'alerte, le UIWindowne répond pas. Quelque chose à voir avec le windowLevelprobablement. Comment puis-je le rendre réactif?
slider

1
On dirait que la nouvelle fenêtre n'a pas été rejetée.
Igor Kulagin

Il semble que la fenêtre ne soit pas supprimée par le haut, il faut donc supprimer la fenêtre une fois terminée.
soan saini

Réglez votre alertWindowà nillorsque vous avez terminé avec elle.
C6Silver

13

En ajoutant à la réponse de Zev (et en revenant à Objective-C), vous pourriez rencontrer une situation où votre contrôleur de vue racine présente un autre VC via un enchaînement ou autre chose. L'appel de PresentViewController sur le VC racine se chargera de cela:

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

Cela a résolu un problème que j'avais lorsque le VC racine avait migré vers un autre VC, et au lieu de présenter le contrôleur d'alerte, un avertissement comme ceux rapportés ci-dessus a été émis:

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

Je ne l'ai pas testé, mais cela peut également être nécessaire si votre VC racine se trouve être un contrôleur de navigation.


Hum je rencontre ce problème dans Swift, et je ne trouve pas comment traduire votre code objc en swift, l'aide serait très appréciée!

2
@Mayerz traduire Objective-C en Swift ne devrait pas être un gros problème;) mais vous êtes ici:UIApplication.sharedApplication().keyWindow?.rootViewController?.presentedViewController?.presentViewController(controller, animated: true, completion: nil)
borchero

Merci Olivier, tu as raison, c'est simple comme bonjour, et je l'ai traduit de cette façon, mais le problème était ailleurs. Merci quand même!

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
Je suis allé avec la même approche, utilisez le rootViewController.presentedViewControllersi ce n'est pas nul, sinon utilisez rootViewController. Pour une solution entièrement générique, il peut être nécessaire de parcourir la chaîne de presentedViewControllers pour arriver au topmostVC
Protongun

9

La réponse de @ agilityvision a été traduite en Swift4 / iOS11. Je n'ai pas utilisé de chaînes localisées, mais vous pouvez facilement changer cela:

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

J'obtenais un fond noir avec la réponse acceptée. window.backgroundColor = UIColor.clearcorrigé cela. viewController.view.backgroundColor = UIColor.clearne semble pas nécessaire.
Ben Patch

Gardez à l'esprit qu'Apple met en garde contre le UIAlertControllersousThe 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.
classement

6

Créer une extension comme dans la réponse Aviel Gross. Ici, vous avez l'extension Objective-C.

Ici, vous avez un fichier d'en-tête * .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

Et implémentation: * .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

Vous utilisez cette extension dans votre fichier d'implémentation comme ceci:

#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 poster ma réponse car ces deux fils ne sont pas marqués comme dupes ...

Maintenant que cela UIViewControllerfait partie de la chaîne de répondeurs, vous pouvez faire quelque chose comme ceci:

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 réponse de Zev Eisenberg est simple et directe, mais elle ne fonctionne pas toujours, et elle peut échouer avec ce message d'avertissement:

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

Cela est dû au fait que le rootViewController Windows n'est pas en haut des vues présentées. Pour corriger cela, nous devons remonter la chaîne de présentation, comme indiqué dans mon code d'extension UIAlertController écrit dans 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)
}

Mises à jour le 15/09/2017:

Testé et confirmé que la logique ci-dessus fonctionne toujours très bien dans la nouvelle graine iOS 11 GM. Cependant, la méthode la plus votée par agilityvision ne le fait pas: la vue d'alerte présentée dans une nouvelle version UIWindowest en dessous du clavier et empêche potentiellement l'utilisateur d'appuyer sur ses boutons. En effet, dans iOS 11, tous les niveaux de fenêtre supérieurs à ceux de la fenêtre du clavier sont abaissés à un niveau inférieur.

Un des artefacts de la présentation keyWindowest cependant l'animation du clavier glissant vers le bas lorsque l'alerte est présentée et remontant lorsque l'alerte est rejetée. Si vous souhaitez que le clavier y reste pendant la présentation, vous pouvez essayer de présenter à partir de la fenêtre supérieure elle-même, comme indiqué dans le code ci-dessous:

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 seule partie moins importante du code ci-dessus est qu'il vérifie le nom UIRemoteKeyboardWindowde la classe pour s'assurer que nous pouvons également l'inclure. Néanmoins, le code ci-dessus fonctionne très bien dans les semences GM iOS 9, 10 et 11, avec la bonne teinte et sans les artefacts coulissants du clavier.


Je viens de passer en revue les nombreuses réponses précédentes ici et j'ai vu la réponse de Kevin Sliech, qui essaie de résoudre le même problème avec une approche similaire, mais qui a arrêté de remonter la chaîne de présentation, ce qui le rend vulnérable à la même erreur qu'il essaie de résoudre. .
CodeBrew

4

Swift 4+

Solution que j'utilise depuis des années sans aucun problème. Tout d'abord, j'étends UIWindowpour trouver qu'il est visibleViewController. REMARQUE : si vous utilisez des classes de collection * personnalisées (telles que le menu latéral), vous devez ajouter un gestionnaire pour ce cas dans l'extension suivante. Après avoir obtenu la plupart des contrôleurs de vue, il est facile de présenter UIAlertControllercomme 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

Pour iOS 13, en s'appuyant sur les réponses de mythicalcoder et bobbyrehm :

Dans iOS 13, si vous créez votre propre fenêtre pour présenter l'alerte, vous devez conserver une référence forte à cette fenêtre, sinon votre alerte ne s'affichera pas car la fenêtre sera immédiatement désallouée lorsque sa référence quittera la portée.

De plus, vous devrez redéfinir la référence sur zéro après la fermeture de l'alerte afin de supprimer la fenêtre pour continuer à permettre l'interaction de l'utilisateur sur la fenêtre principale en dessous.

Vous pouvez créer une UIViewControllersous - classe pour encapsuler la logique de gestion de la mémoire de fenêtre:

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

Vous pouvez l'utiliser tel UIAlertControllerquel , ou si vous voulez une méthode pratique sur votre , vous pouvez la jeter dans une extension:

extension UIAlertController {

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

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

Cela ne fonctionne pas si vous devez fermer l'alerte manuellement - le WindowAlertPresentationController n'est jamais désalloué, ce qui entraîne une interface utilisateur gelée - rien n'est interactif en raison de la fenêtre toujours là
JBlake

Si vous souhaitez annuler l'alerte manuellement, assurez-vous d'appeler dismissdirectement le WindowAlertPresentationController alert.presentingViewController?.dismiss(animated: true, completion: nil)
JBlake

let alertController = UIAlertController (title: "title", message: "message", preferStyle: .alert); alertController.presentInOwnWindow (animé: faux, achèvement: nul) fonctionne très bien pour moi! Merci!
Brian

Cela fonctionne sur iPhone 6 avec iOS 12.4.5, mais pas sur iPhone 11 Pro avec iOS 13.3.1. Il n'y a pas d'erreur, mais l'alerte ne s'affiche jamais. Toute suggestion serait appréciée.
jl303

Fonctionne très bien pour iOS 13. Ne fonctionne pas dans Catalyst - une fois l'alerte rejetée, l'application n'est plus capable d'interagir. Voir la solution de
@Peter Lapisu

3

Manière abrégée de présenter l'alerte dans Objective-C:

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

alertControllerest tonUIAlertController objet.

REMARQUE: vous devrez également vous assurer que votre classe d'assistance s'étend UIViewController


3

Si quelqu'un est intéressé, j'ai créé une version Swift 3 de @agilityvision answer. Le code:

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: J'ai annulé votre modification. Cette «gestion des erreurs» est totalement inutile.
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
        }
    }
}

Avec cela, vous pouvez facilement présenter votre alerte comme ça

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

Une chose à noter est que s'il y a un UIAlertController actuellement affiché, UIApplication.topMostViewControllerretournera un UIAlertController. Présenter au-dessus d'un UIAlertControllera un comportement étrange et doit être évité. En tant que tel, vous devez soit vérifier manuellement cela !(UIApplication.topMostViewController is UIAlertController)avant de présenter, soit ajouter un else ifcas pour retourner nil 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
        }
    }
}

1

Vous pouvez envoyer la vue ou le contrôleur actuel en tant que paramètre:

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

Oui, c'est possible et ça marcherait. Mais pour moi, ça a une odeur de code. Les paramètres passés doivent généralement être requis pour que la méthode appelée exécute sa fonction principale. De plus, tous les appels existants devraient être modifiés.
Murray Sagal

1

Kevin Sliech a fourni une excellente solution.

J'utilise maintenant le code ci-dessous dans ma sous-classe principale UIViewController.

Une petite modification que j'ai faite était de vérifier si le meilleur contrôleur de présentation n'est pas un simple UIViewController. Sinon, ce doit être un VC qui présente un VC simple. Ainsi, nous renvoyons le VC qui est présenté à la place.

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

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

    return bestPresentationController;
}

Semble tout fonctionner jusqu'à présent dans mes tests.

Merci Kevin!


1

En plus de bonnes réponses données ( agilityvision , adib , malhal ). Pour atteindre un comportement de mise en file d'attente comme dans les bons vieux UIAlertViews (éviter le chevauchement des fenêtres d'alerte), utilisez ce bloc pour observer la disponibilité au niveau de la fenêtre:

@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

Exemple complet:

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

Cela vous permettra d'éviter le chevauchement des fenêtres d'alerte. La même méthode peut être utilisée pour séparer et mettre en file d'attente des contrôleurs de vue pour un nombre quelconque de couches de fenêtre.


1

J'ai essayé tout ce qui a été mentionné, mais sans succès. La méthode que j'ai utilisée pour 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

Certaines de ces réponses n'ont fonctionné qu'en partie pour moi, les combiner dans la méthode de classe suivante dans AppDelegate était la solution pour moi. Il fonctionne sur iPad, dans les vues UITabBarController, dans UINavigationController, en lors de la présentation des modaux. Testé sur iOS 10 et 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;
}

Usage:

[[AppDelegate rootViewController] presentViewController ...

1

Prise en charge des scènes iOS13 (lors de l'utilisation de 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 ne doit pas être sous
classé

0

Vous pouvez essayer d'implémenter une catégorie UIViewControlleravec un mehtod comme - (void)presentErrorMessage;Et et à l'intérieur de cette méthode, vous implémentez UIAlertController, puis la présentez self. Que dans votre code client, vous aurez quelque chose comme:

[myViewController presentErrorMessage];

De cette façon, vous éviterez les paramètres inutiles et les avertissements indiquant que la vue n'est pas dans la hiérarchie des fenêtres.


Sauf que je n'ai pas myViewControllerdans le code où la mauvaise chose arrive. C'est dans une méthode utilitaire qui ne sait rien du contrôleur de vue qui l'a appelé.
Murray Sagal

2
IMHO présentant des vues (donc des alertes) à l'utilisateur est la responsabilité de ViewControllers. Donc, si une partie du code ne sait rien sur viewController, il ne devrait pas présenter d'erreurs à l'utilisateur, mais plutôt les transmettre aux parties "viewController aware" du code
Vlad Soroka

2
Je suis d'accord. Mais la commodité du désormais obsolète UIAlertViewm'a amené à enfreindre cette règle à quelques endroits.
Murray Sagal

0

Il existe 2 approches que vous pouvez utiliser:

-Utilisation UIAlertView ou 'UIActionSheet' à la place (non recommandé, car il est obsolète dans iOS 8 mais cela fonctionne maintenant)

- Souvenez-vous du dernier contrôleur de vue qui est présenté. Voici un exemple.

@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 

Usage:

[[UIViewController topViewController] presentViewController:alertController ...];

0

J'utilise ce code avec quelques petites variations personnelles dans ma 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

Semble fonctionner:

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

créer la classe d'assistance AlertWindow et l'utiliser comme

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);
    }
}
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.