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];