问题
I'm trying to have an helper class that presents an UIAlertController
. Since it's a helper class, I want it to work regardless of the view hierarchy, and with no information about it. I'm able to show the alert, but when it's being dismissed, the app crashed with:
*** Terminating app due to uncaught exception 'NSInternalInconsistencyException',
reason: 'Trying to dismiss UIAlertController <UIAlertController: 0x135d70d80>
with unknown presenter.'
I'm creating the popup with:
guard let window = UIApplication.shared.keyWindow else { return }
let view = UIView()
view.isUserInteractionEnabled = true
window.insertSubview(view, at: 0)
window.bringSubview(toFront: view)
// add full screen constraints to view ...
let controller = UIAlertController(
title: "confirm deletion?",
message: ":)",
preferredStyle: .alert
)
let deleteAction = UIAlertAction(
title: "yes",
style: .destructive,
handler: { _ in
DispatchQueue.main.async {
view.removeFromSuperview()
completion()
}
}
)
controller.addAction(deleteAction)
view.insertSubview(controller.view, at: 0)
view.bringSubview(toFront: controller.view)
// add centering constraints to controller.view ...
When I tap yes
, the app will crash and the handler is not being hit before the crash. I can't present the UIAlertController
because this would be dependent of the current view hierarchy, while I want the popup to be independant
EDIT: Swift solution Thanks @Vlad for the idea. It seems that operating in a separate window is much more simple. So here is a working Swift solution:
class Popup {
private var alertWindow: UIWindow
static var shared = Popup()
init() {
alertWindow = UIWindow(frame: UIScreen.main.bounds)
alertWindow.rootViewController = UIViewController()
alertWindow.windowLevel = UIWindowLevelAlert + 1
alertWindow.makeKeyAndVisible()
alertWindow.isHidden = true
}
private func show(completion: @escaping ((Bool) -> Void)) {
let controller = UIAlertController(
title: "Want to do it?",
message: "message",
preferredStyle: .alert
)
let yesAction = UIAlertAction(
title: "Yes",
style: .default,
handler: { _ in
DispatchQueue.main.async {
self.alertWindow.isHidden = true
completion(true)
}
})
let noAction = UIAlertAction(
title: "Not now",
style: .destructive,
handler: { _ in
DispatchQueue.main.async {
self.alertWindow.isHidden = true
completion(false)
}
})
controller.addAction(noAction)
controller.addAction(yesAction)
self.alertWindow.isHidden = false
alertWindow.rootViewController?.present(controller, animated: false)
}
}
回答1:
Update Dec 16, 2019:
Just present the view controller/alert from the current top-most view controller. That will work :)
if #available(iOS 13.0, *) {
if var topController = UIApplication.shared.keyWindow?.rootViewController {
while let presentedViewController = topController.presentedViewController {
topController = presentedViewController
}
topController.present(self, animated: true, completion: nil)
}
Update July 23, 2019:
Apparently this technique stopped working in iOS 13.0. :(
I'll update once I find the time to investigate...
Old technique:
Here's a Swift (5) extension for it:
public extension UIAlertController {
func show() {
let win = UIWindow(frame: UIScreen.main.bounds)
let vc = UIViewController()
vc.view.backgroundColor = .clear
win.rootViewController = vc
win.windowLevel = UIWindow.Level.alert + 1 // Swift 3-4: UIWindowLevelAlert + 1
win.makeKeyAndVisible()
vc.present(self, animated: true, completion: nil)
}
}
Just setup your UIAlertController, and then call:
alert.show()
No more bound by the View Controllers hierarchy!
回答2:
I will rather present it on UIApplication.shared.keyWindow.rootViewController, instead of using your logic. So you can do next:
UIApplication.shared.keyWindow.rootViewController.presentController(yourAlert, animated: true, completion: nil)
EDITED:
I have an old ObjC category, where I've used the next method show, which I used, if no controller was provided to present from:
- (void)show
{
self.alertWindow = [[UIWindow alloc] initWithFrame: [UIScreen mainScreen].bounds];
self.alertWindow.rootViewController = [UIViewController new];
self.alertWindow.windowLevel = UIWindowLevelAlert + 1;
[self.alertWindow makeKeyAndVisible];
[self.alertWindow.rootViewController presentViewController: self animated: YES completion: nil];
}
added entire category, if somebody need it
#import "UIAlertController+ShortMessage.h"
#import <objc/runtime.h>
@interface UIAlertController ()
@property (nonatomic, strong) UIWindow* alertWindow;
@end
@implementation UIAlertController (ShortMessage)
- (void)setAlertWindow: (UIWindow*)alertWindow
{
objc_setAssociatedObject(self, @selector(alertWindow), alertWindow, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (UIWindow*)alertWindow
{
return objc_getAssociatedObject(self, @selector(alertWindow));
}
+ (UIAlertController*)showShortMessage: (NSString*)message fromController: (UIViewController*)controller
{
return [self showAlertWithTitle: nil shortMessage: message fromController: controller];
}
+ (UIAlertController*)showAlertWithTitle: (NSString*)title shortMessage: (NSString*)message fromController: (UIViewController*)controller
{
return [self showAlertWithTitle: title shortMessage: message actions: @[[UIAlertAction actionWithTitle: @"Ok" style: UIAlertActionStyleDefault handler: nil]] fromController: controller];
}
+ (UIAlertController*)showAlertWithTitle: (NSString*)title shortMessage: (NSString*)message actions: (NSArray<UIAlertAction*>*)actions fromController: (UIViewController*)controller
{
UIAlertController* alert = [UIAlertController alertControllerWithTitle: title
message: message
preferredStyle: UIAlertControllerStyleAlert];
for (UIAlertAction* action in actions)
{
[alert addAction: action];
}
if (controller)
{
[controller presentViewController: alert animated: YES completion: nil];
}
else
{
[alert show];
}
return alert;
}
+ (UIAlertController*)showAlertWithMessage: (NSString*)message actions: (NSArray<UIAlertAction*>*)actions fromController: (UIViewController*)controller
{
return [self showAlertWithTitle: @"" shortMessage: message actions: actions fromController: controller];
}
- (void)show
{
self.alertWindow = [[UIWindow alloc] initWithFrame: [UIScreen mainScreen].bounds];
self.alertWindow.rootViewController = [UIViewController new];
self.alertWindow.windowLevel = UIWindowLevelAlert + 1;
[self.alertWindow makeKeyAndVisible];
[self.alertWindow.rootViewController presentViewController: self animated: YES completion: nil];
}
@end
回答3:
in Swift 4.1 and Xcode 9.4.1
I'm calling alert function from my shared class
//This is my shared class
import UIKit
class SharedClass: NSObject {
static let sharedInstance = SharedClass()
//This is alert function
func alertWindow(title: String, message: String) {
let alertWindow = UIWindow(frame: UIScreen.main.bounds)
alertWindow.rootViewController = UIViewController()
alertWindow.windowLevel = UIWindowLevelAlert + 1
let alert2 = UIAlertController(title: title, message: message, preferredStyle: .alert)
let defaultAction2 = UIAlertAction(title: "OK", style: .default, handler: { action in
})
alert2.addAction(defaultAction2)
alertWindow.makeKeyAndVisible()
alertWindow.rootViewController?.present(alert2, animated: true, completion: nil)
}
private override init() {
}
}
I'm calling this alert function in my required view controller like this.
//I'm calling this function into my second view controller
SharedClass.sharedInstance.alertWindow(title:"Title message here", message:"Description message here")
回答4:
Old approach with adding show()
method and local instance of UIWindow
no longer works on iOS 13 (window is dismissed right away).
Here is UIAlertController
Swift extension which should work on iOS 13:
import UIKit
private var associationKey: UInt8 = 0
extension UIAlertController {
private var alertWindow: UIWindow! {
get {
return objc_getAssociatedObject(self, &associationKey) as? UIWindow
}
set(newValue) {
objc_setAssociatedObject(self, &associationKey, newValue, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN)
}
}
func show() {
self.alertWindow = UIWindow.init(frame: UIScreen.main.bounds)
self.alertWindow.backgroundColor = .red
let viewController = UIViewController()
viewController.view.backgroundColor = .green
self.alertWindow.rootViewController = viewController
let topWindow = UIApplication.shared.windows.last
if let topWindow = topWindow {
self.alertWindow.windowLevel = topWindow.windowLevel + 1
}
self.alertWindow.makeKeyAndVisible()
self.alertWindow.rootViewController?.present(self, animated: true, completion: nil)
}
override open func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
self.alertWindow.isHidden = true
self.alertWindow = nil
}
}
Such UIAlertController
then can be created and shown like this:
let alertController = UIAlertController(title: "Title", message: "Message", preferredStyle: .alert)
let alertAction = UIAlertAction(title: "Title", style: .default) { (action) in
print("Action")
}
alertController.addAction(alertAction)
alertController.show()
回答5:
Swift 3 example
let alertWindow = UIWindow(frame: UIScreen.main.bounds)
alertWindow.rootViewController = UIViewController()
alertWindow.windowLevel = UIWindowLevelAlert + 1
let alert = UIAlertController(title: "AlertController Tutorial", message: "Submit something", preferredStyle: .alert)
alertWindow.makeKeyAndVisible()
alertWindow.rootViewController?.present(alert, animated: true, completion: nil)
回答6:
The often cited solution using a newly created UIWindow
as a UIAlertController
extension stopped working in iOS 13 Betas (looks like there is no strong reference held by iOS to the UIWindow
anymore, so the alert disappears immediately).
The below solution is slightly more complex, but works in iOS 13.0 and older versions of iOS:
class GBViewController: UIViewController {
var didDismiss: (() -> Void)?
override func dismiss(animated flag: Bool, completion: (() -> Void)?)
{
super.dismiss(animated: flag, completion:completion)
didDismiss?()
}
override var prefersStatusBarHidden: Bool {
return true
}
}
class GlobalPresenter {
var globalWindow: UIWindow?
static let shared = GlobalPresenter()
private init() {
}
func present(controller: UIViewController) {
globalWindow = UIWindow(frame: UIScreen.main.bounds)
let root = GBViewController()
root.didDismiss = {
self.globalWindow?.resignKey()
self.globalWindow = nil
}
globalWindow!.rootViewController = root
globalWindow!.windowLevel = UIWindow.Level.alert + 1
globalWindow!.makeKeyAndVisible()
globalWindow!.rootViewController?.present(controller, animated: true, completion: nil)
}
}
Usage
let alert = UIAlertController(title: "Alert Test", message: "Alert!", preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "OK", style: .default, handler: nil))
GlobalPresenter.shared.present(controller: alert)
回答7:
func windowErrorAlert(message:String){
let alert = UIAlertController(title: "Error", message: message, preferredStyle: .alert)
let window = UIWindow(frame: UIScreen.main.bounds)
window.rootViewController = UIViewController()
let okAction = UIAlertAction(title: "Ok", style: .default) { (action) -> Void in
alert.dismiss(animated: true, completion: nil)
window.resignKey()
window.isHidden = true
window.removeFromSuperview()
window.windowLevel = UIWindowLevelAlert - 1
window.setNeedsLayout()
}
alert.addAction(okAction)
window.windowLevel = UIWindowLevelAlert + 1
window.makeKeyAndVisible()
window.rootViewController?.present(alert, animated: true, completion: nil)
}
Create a UIAlertController on top of all view and also dismiss and give focus back to your rootViewController.
回答8:
My own iOS 13 workaround.
Rather than subclassing an UIViewController (Andreas solution) or adding associated object (Maxim solution), I'm using a very simple subclass of UIWindow, declared in the category (extension) file of UIAlertController:
@interface AlertWindow : UIWindow
@property (nonatomic) UIWindow *autoretain;
@end
@implementation AlertWindow
@end
Then in the show method :
AlertWindow *win = [[AlertWindow alloc] initWithFrame:UIScreen.mainScreen.bounds];
win.autoretain = win;
… //code as usual
and finally, to release:
- (void)viewWillDisappear:(BOOL)animated {
AlertWindow *win = (AlertWindow *)self.view.window;
if ([win isKindOfClass:AlertWindow.class]) win.autoretain = nil;
[super viewWillDisappear:animated];
}
I do realise that the OP tagged Swift and this is ObjC, but this is so straightforward to adapt…
回答9:
working solution for TVOS 13 and iOS 13
static func showOverAnyVC(title: String, message: String) {
let alert = UIAlertController(title: title, message: message, preferredStyle: UIAlertController.Style.alert)
alert.addAction((UIAlertAction(title: "OK", style: .default, handler: {(action) -> Void in
})))
let appDelegate = UIApplication.shared.delegate as! AppDelegate
var topController: UIViewController = appDelegate.window!.rootViewController!
while (topController.presentedViewController != nil) {
topController = topController.presentedViewController!
}
topController.present(alert, animated: true, completion: nil)
}
来源:https://stackoverflow.com/questions/40991450/ios-present-uialertcontroller-on-top-of-everything-regardless-of-the-view-hier