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
?
À 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
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)
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
. 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)
}
}
}
}
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:
window.makeKeyAndVisible()
)window.rootViewController = UIViewController()
)Quelques choses à noter:
window.windowLevel = UIWindowLevelAlert + 1
)Enfin, j'ai une mise en œuvre terminée si vous voulez simplement regarder cela.
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];
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 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 dans Swift 3.
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)
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")
})
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.
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)
}
}
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];
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.
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)
}
Méthode abrégée pour présenter l'alerte dans Objective-C:
[[[[UIApplication sharedApplication] keyWindow] rootViewController] presentViewController:alertController animated:YES completion:nil];
Où alertController
est votre objet UIAlertController
.
REMARQUE: vous devrez également vous assurer que votre classe d'assistance 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 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
}
}
}
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
}
}
}
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
}
}
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.
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.
}
Une autre option:
var topController:UIViewController = UIApplication.shared.keyWindow!.rootViewController!
while ((topController.presentedViewController) != nil) {
topController = topController.presentedViewController!
}
topController.present(alert, animated:true, completion:nil)
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 ...];
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!
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];
}
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)
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
}
}
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;
}
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);
}
}
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.