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
?
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
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)
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)
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)
}
}
}
}
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:
- Faire de votre fenêtre de la clé et visible de la fenêtre (
window.makeKeyAndVisible()
) - il suffit d'utiliser un simple UIViewController instance comme le rootViewController de la nouvelle fenêtre. (
window.rootViewController = UIViewController()
) - 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.
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];
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.
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)
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")
})
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.
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];
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)
}
}
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)
}
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.
Raccourci façon de faire présente l'alerte en Objective-C:
[[[[UIApplication sharedApplication] keyWindow] rootViewController] presentViewController:alertController animated:YES completion:nil];
où alertController
est votre objet UIAlertController
.
NOTE: vous devez également vous assurer que votre classe helper s'étend UIViewController
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
}
}
}
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
}
}
}
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.
}
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.
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
}
}
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.
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 ...];
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!
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;
}
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];
}
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);
}
}
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
}
}
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)
}
}
}
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)
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.