并发UIAlertControllers

问题描述 投票:15回答:10

我正在将我的应用程序移植到iOS 8.0,并注意到UIAlertView已被弃用。

所以我改变了使用UIAlertController的东西。在大多数情况下都有效。

除了,当我的应用程序打开时,它会进行多次检查以向用户报告各种状态...

例如......“警告,你没有设置X并且在完成项目之前需要做Y”和“警告,你正在使用测试版并且不依赖于结果”等......(这些只是示例!)

在UIAlertView下,我会(比如说)同时获得两个警告框,用户必须点击两次才能解除两个...但它们都出现了。

在UIAlertController下面,使用下面的代码来显示“常规”警报,我只收到一条警报消息以及一条控制台消息:

警告:尝试在TestViewController上呈现UIAlertController:0x13f667bb0:0x13f63cb40已经呈现UIAlertController:0x13f54edf0

所以,虽然上面可能不是一个很好的例子,但我想有时可能需要在操作应用程序时因“事件”而提出多个全局警报。在旧的UIAlertView下,它们会出现,但似乎它们不会在UIAlertController下。

任何人都可以建议如何使用UIAlertController实现这一目标?

谢谢

+(void)presentAlert:(NSString*)alertMessage withTitle:(NSString*)title
{
    UIAlertController *alertView = [UIAlertController
                                    alertControllerWithTitle:title
                                    message:alertMessage
                                    preferredStyle:UIAlertControllerStyleAlert];

    UIAlertAction* ok = [UIAlertAction
                         actionWithTitle:kOkButtonTitle
                         style:UIAlertActionStyleDefault
                         handler:^(UIAlertAction * action)
                         {
                             //Do some thing here
                             [alertView dismissViewControllerAnimated:YES completion:nil];
                         }];

    [alertView addAction:ok];

    UIViewController *rootViewController = [[[UIApplication sharedApplication] delegate] window].rootViewController;
    [rootViewController presentViewController:alertView animated:YES completion:nil];

编辑:我注意到在iOS8上,连续呈现两个AlertViews,它们“排队”并按顺序出现,而在iOS7中,它们同时出现。似乎Apple已经改变了UIAlertView来排队多个实例。是否有办法使用UIAlertController执行此操作而不继续使用(已弃用但已修改)UIAlertView ???

ipad ios8 uialertview uialertcontroller
10个回答
4
投票

当谈到呈现它时,我也面临着UIAlertController的一些问题。现在,我可以建议的唯一解决方案是从最顶层的presentContrller呈现警报控制器(如果有)或窗口的rootViewController。

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

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

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

您获得的警告不仅限于UIAlertController。视图控制器(在您的情况下是窗口的rootViewController)一次只能显示一个视图控制器。


-1
投票

我用这行代码解决了这个问题:

alert.modalTransitionStyle=UIModalPresentationOverCurrentContext;

4
投票

我完全理解这里的问题,并通过一个UIAlertController类别提出了以下解决方案。它的设计使得如果已经呈现警报,则它会延迟显示下一个警报,直到它收到第一个已被解除的通知。

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

此代码甚至处理通过已弃用的UIAlertView呈现先前警报的情况,即它等待它也完成。

要测试这一点,您需要做的就是连续两次使用两个不同的警报控制器调用show,您将看到第二个等待直到第一个被呈现之前被解雇。


2
投票

这个解决方案对我有用。我有一个AlertManager,它处理一个接一个地呈现的警报队列。要知道何时提出另一个警报,我正在扩展UIAlertController并覆盖其viewDidDisappear函数。

必须在viewDidAppear之后使用此解决方案。如果不是,则不会显示警报。链条将被打破,不再提供进一步的警报。另一个选择是稍后尝试挂起警报或丢弃它,这将释放队列以备将来警报。

/// 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:)

1
投票

我对这里的任何解决方案都不满意,因为他们需要太多的手动工作或需要调配,而我在生产应用程序中并不满意。我创建了一个新类(GitHub),它从这里获取其他答案的元素。

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

用法示例

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

0
投票

这可以通过在UIAlertcontroller的动作处理程序中使用check标志来解决。

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

0
投票

我创建了一个Github项目MAAlertPresenter,带有一个演示来处理这个问题。您可以使用它来逐个呈现UIAlertController,只需几行更改。


0
投票

这似乎是一个老问题,但仍然发布,因为它可能对寻找此事的人有用,虽然Apple不建议多个警报堆叠,这就是为什么他们将此UIAlertView弃用到UIAlertController实现。

我为UIAlertAction创建了一个AQAlertAction子类。您可以将它用于交错警报,其用法与使用UIAlertAction相同。您需要做的就是在项目中导入AQMutiAlertFramework,或者您也可以包括类(请参考Sample project)。在内部它使用二进制信号量来交错警报,直到用户处理与当前警报相关联的操作。请让我知道这对你有没有用。


0
投票

从UIAlertView切换到UIAlertController后,我也遇到了同样的问题。我不喜欢Apple政策,因为“消息框”几乎可以在BIG BANG的所有SO中叠加。我同意并发警报不是一个很好的用户体验,有时它是一个糟糕的设计的结果,但有时(前UILocalNotification或类似的东西)他们只是可能发生,我害怕我可以松开一个重要的阻止警报只是因为我的应用程序刚刚收到通知。

也就是说,这是我的2cents解决方案,一个递归函数,如果发送者没有presentViewController则试图在发送者上呈现alertcontroller,否则它会尝试在presentViewController上呈现alertcontroller等等...如果你不行因为你不能从正在呈现的控制器中呈现一个viewcontroller但是它应该在任何其他合理的工作流程中工作,所以同时激发更多的AlertController。

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

0
投票

如果你需要的只是简单读取和解读的简单信息警报,那么这就是我刚刚提出的(它不是完全花哨的,高级代码并且涉及'耦合',但是,嘿......它很短/ simple,在某些情况下可能有用):

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)
    }

}
© www.soinside.com 2019 - 2024. All rights reserved.