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

scénario: l'utilisateur tape sur un bouton sur un contrôleur de vue. Le contrôleur de vue est le plus haut (évidemment) de la pile de navigation. Le robinet invoque une méthode de classe utilitaire appelée sur une autre classe. Une mauvaise chose se produit là et je veux afficher une alerte juste là avant que le contrôle retourne 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ésenter un UIAlertController , juste là dans myUtilityMethod ?

217
demandé sur Murray Sagal 2014-10-24 23:24:48

30 réponses

au WWDC, je me suis arrêté dans un des labos 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 reçu cette question et nous avons plaisanté en disant qu'ils auraient dû avoir une séance à ce sujet. Il a dit qu'à L'interne Apple crée un UIWindow avec un UIViewController transparent et présente ensuite le UIAlertController sur elle. En gros, ce qu'il y a dans la réponse de Dylan Betterman.

Mais Je Je ne voulais pas utiliser une sous-classe de UIAlertController parce que cela me demanderait de changer mon code dans toute mon application. Donc, avec l'aide d'un objet associé, j'ai fait une catégorie sur UIAlertController qui fournit une méthode show dans L'objectif-C.

voici le code correspondant:

#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'usage:

// 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 UIWindow qui est créé sera détruit lorsque le UIAlertController est désalloué, puisqu'il est le seul objet qui conserve le UIWindow . Mais si vous assignez le UIAlertController à une propriété ou faites augmenter son compte de retenue en accédant à l'alerte dans un des blocs d'action, le UIWindow restera à l'écran, verrouillant votre UI. Voir le code d'utilisation de l'échantillon ci-dessus pour éviter dans le cas de la nécessité d'accéder à UITextField .

j'ai fait un GitHub repo avec un projet d'essai: Ffglobalaltcontroller

285
répondu agilityvision 2018-10-12 07:47:57

vous pouvez faire ce qui suit 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)
88
répondu Zev Eisenberg 2016-09-12 16:43:20

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

Swift 2.3

let alertController = UIAlertController(title: "title", message: "message", preferredStyle: .Alert)
//...
var rootViewController = UIApplication.sharedApplication().keyWindow?.rootViewController
if let navigationController = rootViewController as? UINavigationController {
    rootViewController = navigationController.viewControllers.first
}
if let tabBarController = rootViewController as? UITabBarController {
    rootViewController = tabBarController.selectedViewController
}
rootViewController?.presentViewController(alertController, animated: true, completion: nil)

Swift 3

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)
82
répondu Darkngs 2017-02-28 15:29:08

Assez générique UIAlertController extension pour tous les cas de UINavigationController et/ou UITabBarController . Fonctionne aussi s'il y a un modal VC à l'écran en ce moment.

Utilisation:

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

C'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)
          }
        }
    }
}
31
répondu Aviel Gross 2016-06-09 09:49:20

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

la solution est d'utiliser une Oiwindow supplémentaire.

quand vous voulez afficher votre UIAlertController:

  1. Faire de votre fenêtre de la clé et visible de la fenêtre ( window.makeKeyAndVisible() )
  2. il suffit d'utiliser un simple UIViewController instance comme le rootViewController de la nouvelle fenêtre. ( window.rootViewController = UIViewController() )
  3. Présenter votre UIAlertController sur votre fenêtre rootViewController

deux choses à noter:

  • votre Oiwindow doit être fortement référencé. Si elle n'est pas fortement référencé, il n'apparaîtra jamais (parce qu'il est sorti). Je recommande d'utiliser une propriété, mais j'ai aussi eu du succès avec un objet associé .
  • pour m'assurer que la fenêtre apparaisse au-dessus de tout le reste (y compris les Controllers système UIAlertControllers), je mets 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

29
répondu Dylan Bettermann 2017-05-23 12:34:53

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

Toutefois tant que vous avez une action dans votre alerte contrôleur, vous n'avez pas besoin de conserver une référence à la fenêtre . Comme une étape finale du bloc de handler action, vous avez juste besoin de cacher la fenêtre dans le cadre de la tâche de nettoyage. En ayant un référence à la fenêtre dans le bloc handler, cela crée une référence circulaire temporaire qui serait rompue une fois que le contrôleur d'alerte est congédié.

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];
22
répondu adib 2017-05-23 12:26:36

la solution suivante a fait pas travail bien qu'il semble très prometteur avec toutes les versions. cette solution génère un avertissement .

Avertissement: Tentative de présenter sur dont la vue n'est pas dans la fenêtre hiérarchie!

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

c'est un code assez entièrement fonctionnel en lui-même une fois que vous collez à l'intérieur de n'importe quelle fonction.

Rapide Swift 3 autonome code

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 avec le code Swift 3.

17
répondu mythicalcoder 2017-05-23 10:31:37

cela fonctionne dans Swift pour les contrôleurs de vue normale et même s'il y a un contrôleur de navigation sur 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)
16
répondu William Entriken 2018-07-10 09:31:24

Voici mythicalcoder la réponse de comme une extension, testé et travailler en Swift 4:

extension UIAlertController {

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

}

exemple d'usage:

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")
})
12
répondu bobbyrehm 2017-10-24 16:35:32

en ajoutant à la réponse de Zev (et en revenant à Objective-C), vous pourriez tomber dans une situation où votre contrôleur de vue racine présente une autre VC via une séquence ou quelque chose d'autre. En appelant presentedViewController sur la racine VC, vous vous occuperez de ceci:

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

cela a permis de régler un problème que j'ai eu lorsque le VC racine avait fait la transition vers un autre VC, et au lieu de présenter le contrôleur d'alerte, un avertissement comme ceux qui sont signalé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 aussi être nécessaire si votre root VC se trouve être un contrôleur de navigation.

11
répondu Kevin Sliech 2014-12-25 17:03:06

Créer une Extension comme dans Aviel Brut réponse. Ici vous avez l'extension de L'objectif-C.

ici vous avez le 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 mise en œuvre: *.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 de mise en œuvre 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];
6
répondu Marcin Kapusta 2015-06-03 11:50:38

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

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)
    }
}
5
répondu Dylan Colaco 2018-01-10 16:59:05

Croix de poster mon réponse depuis ces deux threads sont pas considérés comme des doublons...

maintenant que UIViewController fait partie de la chaîne des répondants, 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
répondu Mark Aufflick 2017-05-23 12:26:36

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>

c'est parce que le controller windows rootViewController n'est pas au sommet des vues présentées. Pour corriger cela, nous devons remonter la chaîne de présentation, comme le montre 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 sur 9/15/2017:

a testé et confirmé que la logique ci-dessus fonctionne toujours très bien dans la nouvelle graine iOS 11 GM disponible. La méthode votée en haut par agilityvision, cependant, ne le fait pas: la vue d'alerte présentée dans un nouveau UIWindow est sous le clavier et empêche potentiellement l'utilisateur de taper sur ses boutons. C'est parce que dans iOS 11 tous windowLevels plus élevé que celui de la fenêtre du clavier est ramenée à un niveau inférieur.

un artéfact de présentation de keyWindow bien que c'est l'animation de clavier glissant vers le bas quand l'alerte est présentée, et glissant vers le haut à nouveau quand l'alerte est rejetée. Si vous voulez que le clavier reste là pendant la présentation, vous pouvez essayer de présenter 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 { "151920920".tintColor != nil || "151920920".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 de classe UIRemoteKeyboardWindow pour s'assurer que nous pouvons l'inclure aussi. Néanmoins le code ci-dessus fonctionne très bien dans iOS 9, 10 et 11 GM seed, avec la bonne teinte et sans les artefacts de glissement de clavier.

4
répondu CodeBrew 2017-09-15 17:13:13

Raccourci façon de faire présente l'alerte en Objective-C:

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

alertController est votre objet UIAlertController .

NOTE: vous devez également vous assurer que votre classe helper s'étend UIViewController

3
répondu ViperMav 2017-02-05 21:49:09
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 ceci vous pouvez facilement présenter votre alerte comme ainsi

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

une chose à noter est que s'il y a un UIAlertController actuellement affiché, UIApplication.topMostViewController retournera un UIAlertController . La présentation en haut d'un UIAlertController a un comportement bizarre et devrait être évitée. Par conséquent, vous devriez soit vérifier manuellement que !(UIApplication.topMostViewController is UIAlertController) avant de présenter, ou ajouter un else if cas pour retourner zéro si self 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
        }
    }
}
2
répondu NSExceptional 2017-08-03 05:34:13

Swift 4+

Solution que j'utilise depuis des années sans aucun problème. Tout d'abord je prolonge UIWindow pour trouver c'est visibleViewController. NOTE : si vous utilisez des classes custom collection* (comme le menu latéral), vous devez ajouter handler pour ce cas dans l'extension suivante. Après avoir obtenu le contrôleur de vue le plus haut il est facile de présenter UIAlertController comme 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
    }
  }

}
2
répondu Timur Bernikowich 2017-12-13 16:05:51

vous pouvez envoyer la vue courante ou le contrôleur comme paramètre:

+ (void)myUtilityMethod:(id)controller {
    // do stuff
    // something bad happened, display an alert.
}
1
répondu Pablo A. 2015-04-02 14:26:14

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

@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. Même méthode peut être utilisée pour séparer et mettre dans les contrôleurs de vue de file d'attente pour n'importe quel nombre de couches de fenêtre.

1
répondu Roman B. 2017-05-23 12:34:53

si quelqu'un est intéressé j'ai créé une version Swift 3 de la réponse @agilityvision. 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
    }
}
1
répondu Majster 2018-09-26 07:17:26

vous pouvez essayer de mettre en œuvre une catégorie sur UIViewController avec mehtod comme - (void)presentErrorMessage; et à l'intérieur de cette méthode vous implémentez UIAlertController et ensuite vous le présentez sur self . Que dans votre code client vous aurez quelque chose comme:

[myViewController presentErrorMessage];

de cette façon, vous éviterez les paramétrages et les Avertissements unneccessary sur le fait que view n'est pas dans la hiérarchie des fenêtres.

0
répondu Vlad Soroka 2015-04-07 11:24:21

il y a 2 approches que vous pouvez utiliser:

- utilisez UIAlertView ou "UIActionSheet" à la place (non recommandé, car il déprécié dans iOS 8 mais il fonctionne maintenant)

- rappelez-vous D'une façon ou d'une autre le dernier contrôleur de vue qui est présenté. Ici est 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 

Utilisation:

[[UIViewController topViewController] presentViewController:alertController ...];
0
répondu Sk0prion 2015-09-23 14:05:37

Kevin Sliech a fourni une excellente solution.

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

un petit changement que j'ai fait était de vérifier si le meilleur contrôleur de présentation N'est pas un simple UIViewController. Sinon, ça doit être un VC qui présente un VC pur. Nous retournons donc la CV qui est présentée à la place.

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

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

    return bestPresentationController;
}

semble fonctionner jusqu'à présent dans mes tests.

Merci Kevin!

0
répondu Andrew 2016-03-09 04:02:11

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
répondu Sound Blaster 2016-07-20 23:50:45

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
répondu wonder.mice 2016-10-06 01:32:05

créer de la classe helper AlertWindow et de 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);
    }
}
0
répondu john07 2016-12-27 22:23:22

la réponse de @agilityvision est si bonne. J'ai le sens utilisé dans les projets swift donc j'ai pensé que je partagerais mon point de vue sur sa réponse en utilisant swift 3.0

fileprivate class MyUIAlertController: UIAlertController {

  typealias Handler = () -> Void

  struct AssociatedKeys {
    static var alertWindowKey = "alertWindowKey"
  }

  dynamic var _alertWindow: UIWindow?

  var alertWindow: UIWindow? {
    return objc_getAssociatedObject(self, &AssociatedKeys.alertWindowKey) as? UIWindow
  }


  func setAlert(inWindow window: UIWindow) {
    objc_setAssociatedObject(self, &AssociatedKeys.alertWindowKey, _alertWindow, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
  }

  func show(completion: Handler? = nil) {
    show(animated: true, completion: completion)
  }

  func show(animated: Bool, completion: Handler? =  nil) {
    _alertWindow = UIWindow(frame: UIScreen.main.bounds)
    _alertWindow?.rootViewController = UIViewController()

    if let delegate: UIApplicationDelegate = UIApplication.shared.delegate, let window = delegate.window {
      _alertWindow?.tintColor = window?.tintColor

    }

    let topWindow = UIApplication.shared.windows.last
    _alertWindow?.windowLevel = topWindow?.windowLevel ?? 0 + 1
    _alertWindow?.makeKeyAndVisible()
    _alertWindow?.rootViewController?.present(self, animated: animated, completion: completion)
  }

  fileprivate override func viewDidDisappear(_ animated: Bool) {
    super.viewDidDisappear(animated)
    _alertWindow?.isHidden = true
    _alertWindow = nil
  }
}
0
répondu Justin Wright 2017-03-13 17:01:50

j'ai tout essayé, 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)
        }
    }
}
0
répondu Dragisa Dragisic 2017-12-01 11:26:21

Dans Swift 3

let alertLogin = UIAlertController.init(title: "Your Title", message:"Your message", preferredStyle: .alert)
                                    alertLogin.addAction(UIAlertAction(title: "Done", style:.default, handler: { (AlertAction) in

                                    }))
                                    self.window?.rootViewController?.present(alertLogin, animated: true, completion: nil)
0
répondu Rob-4608 2017-12-01 12:40:40

S'Enregistrer pour un avis avant d'appeler la méthode de classe.

Code Swift:

NSNotificationCenter.defaultCenter().addObserver(self, selector: "displayAlert", name: "ErrorOccured", object: nil)

dans la méthode d'instance displayAlert , vous pouvez afficher votre alerte.

-2
répondu runios 2015-05-09 16:16:04