Concurrent UIAlertControllers

给你一囗甜甜゛ 提交于 2019-11-27 22:12:18

I am also facing some problemls with UIAlertController when it comes to present it. Right now the only solution I can suggest is to present alert controller from top most presentedViewContrller if any or window's rootViewController.

UIViewController *presentingViewController = [[[UIApplication sharedApplication] delegate] window].rootViewController;

while(presentingViewController.presentedViewController != nil)
{
    presentingViewController = presentingViewController.presentedViewController;
}

[presentingViewController presentViewController:alertView animated:YES completion:nil];

The warning you are getting is not just limited to UIAlertController. A view controller(window's rootViewController in your case) can present only one view controller at a time.

I fully understand the issue here and came up with the following solution via a category of UIAlertController. It's designed so that if an alert is already being presented, it delays showing of the next alert until it receives a notification that the first has been dismissed.

UIAlertController+MH.h

#import <UIKit/UIKit.h>

@interface UIAlertController (MH)

// Gives previous behavior of UIAlertView in that alerts are queued up.
-(void)mh_show;

@end

UIAlertController+MH.m

@implementation UIAlertController (MH)

// replace the implementation of viewDidDisappear via swizzling.
+ (void)load {
    static dispatch_once_t once_token;
    dispatch_once(&once_token,  ^{
        Method originalMethod = class_getInstanceMethod(self, @selector(viewDidDisappear:));
        Method extendedMethod = class_getInstanceMethod(self, @selector(mh_viewDidDisappear:));
        method_exchangeImplementations(originalMethod, extendedMethod);
    });
}

-(UIWindow*)mh_alertWindow{
    return objc_getAssociatedObject(self, "mh_alertWindow");
}

-(void)mh_setAlertWindow:(UIWindow*)window{
    objc_setAssociatedObject(self, "mh_alertWindow", window, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

-(void)mh_show{
    void (^showAlert)() = ^void() {
        UIWindow* w = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
        // we need to retain the window so it can be set to hidden before it is dealloced so the observation fires.
        [self mh_setAlertWindow:w];
        w.rootViewController = [[UIViewController alloc] init];
        w.windowLevel = UIWindowLevelAlert;
        [w makeKeyAndVisible];
        [w.rootViewController presentViewController:self animated:YES completion:nil];
    };

    // check if existing key window is an alert already being shown. It could be our window or a UIAlertView's window.
    UIWindow* keyWindow = [UIApplication sharedApplication].keyWindow;
    if(keyWindow.windowLevel == UIWindowLevelAlert){
        // if it is, then delay showing this new alert until the previous has been dismissed.
        __block id observer;
        observer = [[NSNotificationCenter defaultCenter] addObserverForName:UIWindowDidBecomeHiddenNotification object:keyWindow queue:nil usingBlock:^(NSNotification * _Nonnull note) {
            [[NSNotificationCenter defaultCenter] removeObserver:observer];
            showAlert();
        }];
    }else{
        // otherwise show the alert immediately.
        showAlert();
    }
}

- (void)mh_viewDidDisappear:(BOOL)animated {
    [self mh_viewDidDisappear:animated]; // calls the original implementation
    [self mh_alertWindow].hidden = YES;
}

@end

This code even handles the case where a previous alert was presented via the deprecated UIAlertView, i.e. it waits on it to finish too.

To test this out all you need to do is call show twice in a row with two different alert controllers and you will see the second one waits until the first has been dismissed before being presented.

This solution is working for me. I have an AlertManager that is handling a queue of alerts that present one after another. To know when to present another alert, I am extending UIAlertController and overriding its viewDidDisappear function.

This solution must be used after viewDidAppear. If not, the alert won't be presented. The chain would be broken and no further alerts would be presented. Another option would be to try a hanging alert later or discard it, which would free up the queue for future alerts.

/// This class presents one alert after another.
/// - Attention:  If one of the alerts are not presented for some reason (ex. before viewDidAppear), it will not disappear either and the chain will be broken. No further alerts would be shown.
class AlertHandler {
    private var alertQueue = [UIAlertController]()
    private var alertInProcess: UIAlertController?

    // singleton
    static let sharedAlerts = AlertHandler()
    private init() {}

    func addToQueue(alert: UIAlertController) {
        alertQueue.append(alert)
        handleQueueAdditions()
    }

    private func handleQueueAdditions() {
        if alertInProcess == nil {
            let alert = alertQueue.removeFirst()
            alertInProcess = alert
            UIApplication.sharedApplication().keyWindow?.rootViewController?.presentViewController(alert, animated: true, completion: nil)
        }
    }

    private func checkForNextAlert(alert: UIAlertController) {
        if alert === alertInProcess {
            if alertQueue.count > 0 {
                let alert = alertQueue.removeFirst()
                alertInProcess = alert
                UIApplication.sharedApplication().keyWindow?.rootViewController?.presentViewController(alert, animated: true, completion: nil)
            } else {
                alertInProcess = nil
            }
        }
    }
}

extension UIAlertController {
    public override func viewDidDisappear(animated: Bool) {
        AlertHandler.sharedAlerts.checkForNextAlert(self)
    }
}

AlertHandler.sharedAlerts.addToQueue(alert:)

I wasn't happy with any of the solutions here as they required too much manual work or required swizzling which I'm not comfortable with in a production App. I created a new class (GitHub) which takes elements from other answers here.

AlertQueue.h

//
//  AlertQueue.h
//
//  Created by Nick Brook on 03/02/2017.
//  Copyright © 2018 Nick Brook. All rights reserved.
//

#import <UIKit/UIKit.h>

@protocol AlertQueueAlertControllerDelegate;

@interface AlertQueueAlertController : UIAlertController

/**
 The alert delegate
 */
@property(nonatomic, weak, nullable) id<AlertQueueAlertControllerDelegate> delegate;

/**
 Any relevant user info for this alert
 */
@property(nonatomic, readonly, nullable) NSDictionary * userInfo;

/**
 The view controller that requested the alert be displayed, if one was passed when adding to the queue
 */
@property(nonatomic, weak, readonly, nullable) UIViewController *presentingController;

/**
 Create an alert with a title, message and user info

 @param title The title for the alert
 @param message The message for the alert
 @param userInfo The user info dictionary
 @return An alert
 */
+ (nonnull instancetype)alertControllerWithTitle:(nullable NSString *)title message:(nullable NSString *)message userInfo:(nullable NSDictionary *)userInfo;

/**
 - Warning: This method is not available on this subclass. Use +alertControllerWithTitle:message:userInfo: instead.
 */
+ (nonnull instancetype)alertControllerWithTitle:(nullable NSString *)title message:(nullable NSString *)message preferredStyle:(UIAlertControllerStyle)preferredStyle NS_UNAVAILABLE;

@end

@interface AlertQueue : NSObject

/**
 The queue of alerts including the currently displayed alerts. The current alert is at index 0 and the next alert to be displayed is at 1. Alerts are displayed on a FIFO basis.
 */
@property(nonatomic, readonly, nonnull) NSArray<AlertQueueAlertController *> *queuedAlerts;

/**
 The currently displayed alert
 */
@property(nonatomic, readonly, nullable) AlertQueueAlertController *displayedAlert;

+ (nonnull instancetype)sharedQueue;

/**
 Display an alert, or add to queue if an alert is currently displayed

 @param alert The alert to display
 */
- (void)displayAlert:(nonnull AlertQueueAlertController *)alert;

/**
 Display an alert, or add to queue if an alert is currently displayed

 @param alert The alert to display
 @param userInfo Any relevant information related to the alert for later reference. If a userinfo dictionary already exists on the alert, the dictionaries will be merged with the userinfo here taking precedence on conflicting keys.
 */
- (void)displayAlert:(nonnull AlertQueueAlertController *)alert userInfo:(nullable NSDictionary *)userInfo;

/**
 Display an alert, or add to queue if an alert is currently displayed

 @param alert The alert to display
 @param viewController The presenting view controller, stored on the alert for future reference
 @param userInfo Any relevant information related to the alert for later reference. If a userinfo dictionary already exists on the alert, the dictionaries will be merged with the userinfo here taking precedence on conflicting keys.
 */
- (void)displayAlert:(nonnull AlertQueueAlertController *)alert fromController:(nullable UIViewController *)viewController userInfo:(nullable NSDictionary *)userInfo;

/**
 Cancel a displayed or queued alert

 @param alert The alert to cancel
 */
- (void)cancelAlert:(nonnull AlertQueueAlertController *)alert;

/**
 Cancel all alerts from a specific view controller, useful if the controller is dimissed.

 @param controller The controller to cancel alerts from
 */
- (void)invalidateAllAlertsFromController:(nonnull UIViewController *)controller;

@end

@protocol AlertQueueAlertControllerDelegate <NSObject>

/**
 The alert was displayed

 @param alertItem The alert displayed
 */
- (void)alertDisplayed:(nonnull AlertQueueAlertController *)alertItem;

/**
 The alert was dismissed

 @param alertItem The alert dismissed
 */
- (void)alertDismissed:(nonnull AlertQueueAlertController *)alertItem;

@end

AlertQueue.m

//
//  AlertQueue.m
//  Nick Brook
//
//  Created by Nick Brook on 03/02/2017.
//  Copyright © 2018 Nick Brook. All rights reserved.
//

#import "AlertQueue.h"

@protocol AlertQueueAlertControllerInternalDelegate
@required
- (void)alertQueueAlertControllerDidDismiss:(AlertQueueAlertController *)alert;

@end

@interface AlertQueueAlertController()

@property(nonatomic, strong, nullable) NSDictionary * userInfo;
@property (nonatomic, weak, nullable) id<AlertQueueAlertControllerInternalDelegate> internalDelegate;
@property(nonatomic, weak) UIViewController *presentingController;

@end

@implementation AlertQueueAlertController

+ (instancetype)alertControllerWithTitle:(NSString *)title message:(NSString *)message userInfo:(NSDictionary *)userInfo {
    AlertQueueAlertController *ac = [super alertControllerWithTitle:title message:message preferredStyle:UIAlertControllerStyleAlert];
    ac.userInfo = userInfo;
    return ac;
}

- (void)dismissViewControllerAnimated:(BOOL)flag completion:(void (^)(void))completion {
    [super dismissViewControllerAnimated:flag completion:completion];
}

- (void)viewWillDisappear:(BOOL)animated {
    [super viewWillDisappear:animated];
}

- (void)viewDidDisappear:(BOOL)animated {
    [super viewDidDisappear:animated];
    [self.internalDelegate alertQueueAlertControllerDidDismiss:self];
}

@end

@interface AlertQueue() <AlertQueueAlertControllerInternalDelegate>

@property(nonatomic, strong, nonnull) NSMutableArray<AlertQueueAlertController *> *internalQueuedAlerts;
@property(nonatomic, strong, nullable) AlertQueueAlertController *displayedAlert;
@property(nonatomic, strong) UIWindow *window;
@property(nonatomic, strong) UIWindow *previousKeyWindow;

@end

@implementation AlertQueue

+ (nonnull instancetype)sharedQueue {
    static AlertQueue *sharedQueue = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        sharedQueue = [AlertQueue new];
    });
    return sharedQueue;
}

- (instancetype)init
{
    self = [super init];
    if (self) {
        self.window = [UIWindow new];
        self.window.windowLevel = UIWindowLevelAlert;
        self.window.backgroundColor = nil;
        self.window.opaque = NO;
        UIViewController *rvc = [UIViewController new];
        rvc.view.backgroundColor = nil;
        rvc.view.opaque = NO;
        self.window.rootViewController = rvc;
        self.internalQueuedAlerts = [NSMutableArray arrayWithCapacity:1];
        [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(windowDidBecomeHidden:) name:UIWindowDidBecomeHiddenNotification object:nil];
    }
    return self;
}

- (void)dealloc {
    [[NSNotificationCenter defaultCenter] removeObserver:self];
}

- (void)windowDidBecomeHidden:(NSNotification *)notification {
    [self displayAlertIfPossible];
}

- (void)alertQueueAlertControllerDidDismiss:(AlertQueueAlertController *)alert {
    if(self.displayedAlert != alert) { return; }
    self.displayedAlert = nil;
    [self.internalQueuedAlerts removeObjectAtIndex:0];
    if([alert.delegate respondsToSelector:@selector(alertDismissed:)]) {
        [alert.delegate alertDismissed:(AlertQueueAlertController * _Nonnull)alert];
    }
    [self displayAlertIfPossible];
}

- (void)displayAlertIfPossible {
    UIWindow *keyWindow = [UIApplication sharedApplication].keyWindow;
    if(self.displayedAlert != nil || (keyWindow != self.window && keyWindow.windowLevel >= UIWindowLevelAlert)) {
        return;
    }
    if(self.internalQueuedAlerts.count == 0) {
        self.window.hidden = YES;
        [self.previousKeyWindow makeKeyWindow];
        self.previousKeyWindow = nil;
        return;
    }
    self.displayedAlert = self.internalQueuedAlerts[0];
    self.window.frame = [UIScreen mainScreen].bounds;
    if(!self.window.isKeyWindow) {
        self.previousKeyWindow = UIApplication.sharedApplication.keyWindow;
        [self.window makeKeyAndVisible];
    }
    [self.window.rootViewController presentViewController:(UIViewController * _Nonnull)self.displayedAlert animated:YES completion:nil];
    if([self.displayedAlert.delegate respondsToSelector:@selector(alertDisplayed:)]) {
        [self.displayedAlert.delegate alertDisplayed:(AlertQueueAlertController * _Nonnull)self.displayedAlert];
    }
}

- (void)displayAlert:(AlertQueueAlertController *)alert {
    [self displayAlert:alert userInfo:nil];
}

- (void)displayAlert:(AlertQueueAlertController *)alert userInfo:(NSDictionary *)userInfo {
    [self displayAlert:alert fromController:nil userInfo:userInfo];
}

- (void)displayAlert:(AlertQueueAlertController *)alert fromController:(UIViewController *)viewController userInfo:(NSDictionary *)userInfo {
    if(alert.preferredStyle != UIAlertControllerStyleAlert) { // cannot display action sheets
        return;
    }
    alert.internalDelegate = self;
    if(userInfo) {
        if(alert.userInfo) {
            NSMutableDictionary *d = alert.userInfo.mutableCopy;
            [d setValuesForKeysWithDictionary:userInfo];
            alert.userInfo = d;
        } else {
            alert.userInfo = userInfo;
        }
    }
    alert.presentingController = viewController;
    [self.internalQueuedAlerts addObject:alert];
    dispatch_async(dispatch_get_main_queue(), ^{
        [self displayAlertIfPossible];
    });
}

- (void)cancelAlert:(AlertQueueAlertController *)alert {
    if(alert == self.displayedAlert) {
        [self.displayedAlert dismissViewControllerAnimated:YES completion:nil];
    } else {
        [self.internalQueuedAlerts removeObject:alert];
    }
}

- (void)invalidateAllAlertsFromController:(UIViewController *)controller {
    NSArray<AlertQueueAlertController *> *queuedAlerts = [self.internalQueuedAlerts copy];
    for(AlertQueueAlertController *alert in queuedAlerts) {
        if(alert.presentingController == controller) {
            [self cancelAlert:alert];
        }
    }
}

- (NSArray<AlertQueueAlertController *> *)queuedAlerts {
    // returns new array so original can be manipulated (alerts cancelled) while enumerating
    return [NSArray arrayWithArray:_internalQueuedAlerts];
}

@end

Example usage

AlertQueueAlertController *ac = [AlertQueueAlertController alertControllerWithTitle:@"Test1" message:@"Test1" userInfo:nil];
[ac addAction:[UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
    NSLog(@"Alert!");
}]];
[[AlertQueue sharedQueue] displayAlert:ac fromController:self userInfo:nil];

This can be solved by using a check flag in UIAlertcontroller's action handler.

- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
_isShowAlertAgain = YES;
[self showAlert];
}

- (void)showAlert {
UIAlertController *alertController = [UIAlertController alertControllerWithTitle:@"Alert" message:@"This is Alert" preferredStyle:UIAlertControllerStyleAlert];
UIAlertAction *okButton = [UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) {
    [alertController dismissViewControllerAnimated:YES completion:nil];
    if (_isShowAlertAgain) {
        _isShowAlertAgain = NO;
        [self showAlert];
    }
}];
UIAlertAction *cancelButton = [UIAlertAction actionWithTitle:@"Cancel" style:UIAlertActionStyleCancel handler:^(UIAlertAction *action) {
    [alertController dismissViewControllerAnimated:YES completion:nil];
}];
[alertController addAction:okButton];
[alertController addAction:cancelButton];
[self presentViewController:alertController animated:YES completion:nil];
}

I have created a Github project MAAlertPresenter with a demo to handle this issue. You can use it to present UIAlertController one by one with just few lines of changes.

It seems to be old question but still posting as It might be useful for someone looking for this though Apple doesn't recommend multiple alerts stacking thats why they deprecated this UIAlertView from to UIAlertController implementation.

I have created a AQAlertAction subclass for UIAlertAction. You can use it for staggering Alerts, the usage is same as you are using UIAlertAction. All you need to do is import AQMutiAlertFramework in your project or you can include class also (Please refer Sample project for that). Internally It uses binary semaphore for staggering the Alerts until user handle action associated with current alert displayed. Let me know if it works for you.

I also faced the same issue after switching from UIAlertView to UIAlertController. I don't like Apple policy because "Message Boxes" have always been stackable in almost every SO from the BIG BANG. I agree that having concurrent alerts is not a great user experience and sometimes it's the result of a bad design but sometimes (ex UILocalNotification or stuff like that) they just can happen and it's scared that I can loose an important blocking Alert just because my app has just received a notification.

That said, this is my 2cents solution, a recursive function which tries to present the alertcontroller on the sender if the sender has no presentedViewController, otherwise it tries to present the alertcontroller on the presentedViewController and so on... It does not work if you fire more AlertController exactly at the same time because you can't present a viewcontroller from a controller which is being presented but it should work in any other reasonable workflow.

+ (void)presentAlert:(UIAlertController *)alert withSender:(id)sender
{
    if ([sender presentedViewController])
    {
        [self presentAlert:alert withSender: [sender presentedViewController]];
    }
    else
    {
        [sender presentViewController:alert animated:YES completion:nil];
    }
}

If all you need is simple information alerts that are simply read and dismissed, then this is what I just came up with (it's not exactly fancy, high-level code and there is 'coupling' involved, but, hey... it's short/simple and may be useful in some cases):

ReadOnlyMessageQueue.swift:

import Foundation

protocol ReadOnlyMessageQueueDelegate: class {
    func showAlert(message: String, title: String)
}

class ReadOnlyMessageQueue {

    weak var delegate: ReadOnlyMessageQueueDelegate?

    private var queue = [(message: String, title: String)]()

    public func addAlertMessageToQueue(message: String, title: String) {
        print("MQ.add: \(message)")
        queue.append((message,title))
        if queue.count == 1 {
            delegate?.showAlert(message: message, title: title)
        }
    }

    public func alertWasDismissedInParentVC() {
        print("MQ.wasDissmissed")
        if queue.count > 1 {
            delegate?.showAlert(message: queue[1].message, title: self.queue[1].title)
            self.queue.remove(at: 0)
        } else if queue.count == 1 {
            self.queue.remove(at: 0)
        }
    }

}

ViewController.swift:

import UIKit

class ViewController: UIViewController, ReadOnlyMessageQueueDelegate {

    let messageQueue = ReadOnlyMessageQueue()

    override func viewDidLoad() {
        super.viewDidLoad()
        messageQueue.delegate = self
    }

    override func viewDidAppear(_ animated: Bool) {
        for i in 4...20 {
            print("VC.adding: \(i)")
            messageQueue.addAlertMessageToQueue(message: String(i), title: String(i))
        }
    }

    func showAlert(message: String, title: String) {
        print("VC.showing: \(message)")
        let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
        alert.addAction(UIAlertAction(title: "OK", style: .default, handler: {
            _ in
            self.messageQueue.alertWasDismissedInParentVC()
            }
        ))
        self.present(alert, animated: false)
    }

}

I solved this problem with this line of code :

alert.modalTransitionStyle=UIModalPresentationOverCurrentContext;
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!