web-dev-qa-db-fra.com

Comment présenter UIAlertController lorsqu'il ne se trouve pas dans un contrôleur de vue?

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

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

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

Dans ce cas, comment présentez-vous une UIAlertController, juste là dans myUtilityMethod

224
Murray Sagal

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

Mais je ne voulais pas utiliser une sous-classe de UIAlertController car cela nécessiterait que je modifie mon code dans l'application. Donc, avec l'aide d'un objet associé, j'ai créé une catégorie sur UIAlertController qui fournit une méthode show en 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];

La UIWindow créée sera détruite lorsque la UIAlertController sera désallouée, car il s'agit du seul objet qui conserve la UIWindow. Mais si vous affectez la UIAlertController à une propriété ou si vous augmentez le nombre de retenues en accédant à l'alerte dans l'un des blocs d'action, la UIWindow reste à l'écran et verrouille votre interface utilisateur. Voir l'exemple de code d'utilisation ci-dessus pour éviter d'avoir à accéder à UITextField.

J'ai fait un repo GitHub avec un projet test: FFGlobalAlertController

294
agilityvision

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)
93
Zev Eisenberg

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)
86
Darkngs

Assez générique UIAlertControllerextension pour tous les cas de UINavigationController et/ou UITabBarController. Cela fonctionne aussi s'il y a un modal VC à l'écran pour le 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)
          }
        }
    }
}
33
Aviel Gross

J'ai posté une question similaire il y a quelques mois et 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 consiste à utiliser une UIWindow 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 instance UIViewController en tant que rootViewController de la nouvelle fenêtre. (window.rootViewController = UIViewController())
  3. Présentez votre UIAlertController sur le rootViewController de votre fenêtre

Quelques choses à noter:

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

Enfin, j'ai une mise en œuvre terminée si vous voulez simplement regarder cela.

https://github.com/dbettermann/DBAlertController

30
Dylan Bettermann

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

Cependant tant que vous avez une action dans votre contrôleur d'alertes, vous n'avez pas besoin de conserver une référence à la fenêtre. Lors de la dernière étape 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 brisée une fois que le contrôleur d'alerte est rejeté.

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
adib

La solution suivante fonctionnait bien {not} _ même si elle semblait très prometteuse avec toutes les versions. Cette solution génère un AVERTISSEMENT .

Warning: Essayez de présenter sur la vue dont la vue ne se trouve pas dans la hiérarchie des fenêtres!

https://stackoverflow.com/a/34487871/2369867 => Cela semble donc prometteur. Mais c’était pas dans Swift 3. Je réponds donc à cela dans Swift 3 et c’est un exemple de modèle {non

Ceci est plutôt du code entièrement fonctionnel par lui-même une fois que vous collez à l'intérieur d'une fonction.

Rapide Swift 3autonome 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 dans Swift 3.

20
mythicalcoder

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)
18
William Entriken

Voici la réponse de mythicalcoder en extension, testée & opérationnelle 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")
})
15
bobbyrehm

En ajoutant à la réponse de Zev (et en revenant à Objective-C), vous pourriez vous retrouver dans une situation où votre contrôleur de vue racine présente un autre VC via une séquence ou autre. L'appel de presentsViewController à la racine VC se chargera de ceci:

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

Cela a corrigé un problème que j'avais alors où la racine VC avait été rattachée à un autre VC, et au lieu de présenter le contrôleur d'alerte, un avertissement semblable à 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 racine VC se trouve être un contrôleur de navigation. 

13
Kevin Sliech

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

Créer une extension comme dans la réponse d'Aviel Gross. Ici, vous avez l'extension Objective-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 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];
6
Marcin Kapusta

La réponse de Zev Eisenberg est simple et directe, mais cela ne fonctionne pas toujours et peut échouer avec ce message d'avertissement: 

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

Ceci est dû au fait que le rootViewController de Windows ne se trouve pas en haut des vues présentées. Pour corriger cela, nous devons parcourir 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 graine iOS 11 GM nouvellement disponible. La méthode la plus votée par agilityvision ne le fait cependant pas: la vue d'alerte présentée dans la nouvelle variable UIWindow est située sous le clavier et empêche potentiellement l'utilisateur d'appuyer sur ses boutons. En effet, sous iOS 11, tous les niveaux de fenêtre supérieurs à ceux de la fenêtre du clavier sont abaissés à un niveau inférieur. 

Cependant, un artefact de la présentation de keyWindow est l’animation du clavier qui glisse vers le bas lorsqu’une alerte est présentée et qui remonte à nouveau lorsque cette alerte est supprimé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, 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 pas si grande du code ci-dessus est qu’il vérifie le nom de classe UIRemoteKeyboardWindow pour s’assurer que nous pouvons l’inclure également. Néanmoins, le code ci-dessus fonctionne parfaitement sous iOS 9, 10 et 11 GM, avec la bonne teinte et sans les artefacts glissants du clavier.

4
CodeBrew

Cross post my answer puisque ces deux fils ne sont pas marqués comme dupes ...

Maintenant que UIViewController fait 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
Mark Aufflick

Méthode abrégée pour présenter l'alerte dans Objective-C:

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

alertController est votre objet UIAlertController.

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

3
ViperMav
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 si

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

Une chose à noter est que si un UIAlertController est actuellement affiché, UIApplication.topMostViewController renverra une UIAlertController. Présenter sur une UIAlertController a un comportement étrange et doit être évité. En tant que tel, vous devez soit vérifier manuellement le !(UIApplication.topMostViewController is UIAlertController) avant de le présenter, soit ajouter un else if pour renvoyer nil 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
NSExceptional

Swift 4+

Solution que j'utilise depuis des années sans aucun problème. Tout d'abord, j'étends UIWindow pour qu'il soit visibleViewController.NOTE: si vous utilisez des classes collection * personnalisées (comme le menu latéral), vous devez ajouter un gestionnaire pour ce cas dans l'extension suivante. Après avoir obtenu le meilleur contrôleur de vue, il est facile de présenter UIAlertController, tout 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
Timur Bernikovich

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
Majster

En plus des bonnes réponses données ( agilityvision , adib , malhal ). Pour obtenir un comportement de mise en file d'attente comme dans le bon vieux UIAlertViews (é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. La même méthode peut être utilisée pour séparer et mettre en contrôleur de vue de file d'attente un nombre quelconque de couches de fenêtre.

1
Roman B.

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

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

Une autre option:

    var topController:UIViewController = UIApplication.shared.keyWindow!.rootViewController!
    while ((topController.presentedViewController) != nil) {
        topController = topController.presentedViewController!
    }
    topController.present(alert, animated:true, completion:nil)
0
Mike Koene

Il existe 2 approches que vous pouvez utiliser:

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

-En quelque sorte, souvenez-vous du dernier contrôleur de vue 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
Gralex

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’était pas un simple UIViewController. Si ce n'est pas le cas, il doit s'agir d'un VC présentant un VC simple. Nous renvoyons donc 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;
}

On dirait que tout a fonctionné jusqu'ici dans mes tests.

Merci Kevin!

0
Andrew

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
wonder.mice

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
Dragisa Dragisic

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
Rob-4608

La réponse de @ agilityvision est tellement bonne. J'ai utilisé mes idées dans les projets Swift, j'ai donc pensé partager ma réponse avec 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
Justin Wright

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
Sound Blaster

créer la classe d'assistance AlertWindow et que 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
john07

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

[myViewController presentErrorMessage];

De cette façon, vous éviterez les paramétrages inutiles et les avertissements concernant le fait que la vue ne soit pas dans la hiérarchie des fenêtres.

0
Vlad Soroka