Параллельные UIAlertControllers

Я переношу свое приложение в iOS 8.0 и замечаю, что UIAlertView устарел.

Итак, я изменил все, чтобы использовать UIAlertController. Это работает в большинстве случаев.

Кроме того, когда мое приложение открывается, оно выполняет несколько проверок, чтобы сообщать о различных состояниях пользователю...

Например: "Предупреждение, вы не установили X и не должны выполнять Y перед выполнением проектов" и "Предупреждение, вы используете бета-версию и не полагаетесь на результаты" и т.д. (это просто примеры!)

В UIAlertView я бы (скажем) получил два окна предупреждения одновременно, которые пользователь должен дважды нажать, чтобы уволить оба... но они оба появляются.

В UIAlertController с приведенным ниже кодом для представления "общего" предупреждения, я получаю только одно предупреждающее сообщение вместе с сообщением в консоли:

Предупреждение: попытка представить UIAlertController: 0x13f667bb0 на TestViewController: 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???

Ответ 1

Я также сталкиваюсь с некоторыми проблемами с UIAlertController, когда дело доходит до его представления. Прямо сейчас единственным решением, которое я могу предложить, является предоставление контроллера предупреждений сверху самого представленногоViewContrller, если он есть или окно rootViewController.

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

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

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

Предупреждение, которое вы получаете, не ограничивается только UIAlertController. Контроллер вида (window rootViewController в вашем случае) может одновременно отображать только один контроллер представления.

Ответ 2

Я полностью понимаю проблему здесь и придумал следующее решение через категорию 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 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, то есть оно также ожидает завершения.

Чтобы проверить это, все, что вам нужно сделать, это вызвать вызов дважды в строке с двумя разными диспетчерами предупреждений, и вы увидите, что второй ждет, пока первый не будет уволен до того, как будет представлен.

Ответ 3

Это решение работает для меня. У меня есть 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:)

Ответ 4

Меня не устраивало какое-либо из решений здесь, поскольку они требовали слишком много ручной работы или требовали swizzling, что мне не удобно в производственном приложении. Я создал новый класс (GitHub), который содержит элементы из других ответов здесь.

AlertQueue.h

#import <UIKit/UIKit.h>

@protocol AlertQueueItemDelegate;

@interface AlertQueueItem : NSObject

@property(nonatomic, weak, nullable) id<AlertQueueItemDelegate> delegate;
@property(nonatomic, readonly, nullable) NSDictionary * userInfo;

@end

@interface AlertQueue : NSObject

@property(nonatomic, readonly, nonnull) NSArray<AlertQueueItem *> *queuedAlerts;
@property(nonatomic, readonly, nullable) AlertQueueItem *displayedAlert;

+ (nonnull instancetype)sharedQueue;

- (nonnull AlertQueueItem *)displayAlert:(nonnull UIAlertController *)alert delegate:(nullable id<AlertQueueItemDelegate>)delegate userInfo:(nullable NSDictionary *)userInfo;

- (void)cancelAlert:(nonnull AlertQueueItem *)item;

@end

@protocol AlertQueueItemDelegate <NSObject>

- (void)alertDisplayed:(nonnull AlertQueueItem *)alertItem;
- (void)alertDismissed:(nonnull AlertQueueItem *)alertItem;

@end

AlertQueue.m

#import "AlertQueue.h"

@interface AlertQueue()

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

- (void)alertControllerDismissed:(nonnull UIAlertController *)alert;

@end

@interface AlertViewController : UIAlertController

@end

@implementation AlertViewController

- (void)viewDidDisappear:(BOOL)animated {
    [super viewDidDisappear:animated];
    [[AlertQueue sharedQueue] alertControllerDismissed:self];
}

@end

@interface AlertQueueItem()

@property(nonatomic, strong) AlertViewController *alert;
@property(nonatomic, strong, nullable) NSDictionary * userInfo;

@end

@implementation AlertQueueItem

@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;
        self.window.rootViewController = [[UIViewController alloc] init];
        self.window.rootViewController.view.backgroundColor = nil;
        self.window.rootViewController.view.opaque = NO;
        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)alertControllerDismissed:(UIAlertController *)alert {
    if(alert != self.displayedAlert.alert) {
        return;
    }
    AlertQueueItem *item = self.displayedAlert;
    self.displayedAlert = nil;
    [self.internalQueuedAlerts removeObjectAtIndex:0];
    if([item.delegate respondsToSelector:@selector(alertDismissed:)]) {
        [item.delegate alertDismissed:(AlertQueueItem * _Nonnull)item];
    }
    [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;
        return;
    }
    self.displayedAlert = self.internalQueuedAlerts[0];
    self.window.frame = [UIScreen mainScreen].bounds;
    [self.window makeKeyAndVisible];
    [self.window.rootViewController presentViewController:(AlertViewController * _Nonnull)self.displayedAlert.alert animated:YES completion:nil];
    if([self.displayedAlert.delegate respondsToSelector:@selector(alertDisplayed:)]) {
        [self.displayedAlert.delegate alertDisplayed:(AlertQueueItem * _Nonnull)self.displayedAlert];
    }
}

- (AlertQueueItem *)displayAlert:(UIAlertController *)alert delegate:(id<AlertQueueItemDelegate>)delegate userInfo:(id)userInfo {
    AlertQueueItem * item = [AlertQueueItem new];
    item.alert = [AlertViewController alertControllerWithTitle:alert.title message:alert.message preferredStyle:alert.preferredStyle];
    for(UIAlertAction *a in alert.actions) {
        [item.alert addAction:a];
    }
    [self.internalQueuedAlerts addObject:item];
    dispatch_async(dispatch_get_main_queue(), ^{
        [self displayAlertIfPossible];
    });
    return item;
}

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

- (NSArray<AlertQueueItem *> *)queuedAlerts {
    return _internalQueuedAlerts;
}

@end

Пример использования

UIAlertController *ac = [UIAlertController alertControllerWithTitle:@"Test1" message:@"Test1" preferredStyle:UIAlertControllerStyleAlert];
[ac addAction:[UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleDefault handler:nil]];
AlertQueueItem *item = [[AlertQueue sharedQueue] displayAlert:ac delegate:nil userInfo:nil];

Ответ 5

Это можно решить, используя флаг проверки в обработчике действий UIAlertcontroller.

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

Ответ 6

Я создал проект Github MAAlertPresenter с демонстрацией для решения этой проблемы. Вы можете использовать его для представления UIAlertController один за другим с несколькими строками изменений.

Ответ 7

Кажется, старый вопрос, но все равно, поскольку он может быть полезен для кого-то, кто ищет это, хотя Apple не рекомендует многократные оповещения, поэтому они устарели от этого UIAlertView от реализации UIAlertController.

Я создал подкласс AQAlertAction для UIAlertAction. Вы можете использовать его для ошеломляющих предупреждений, использование такое же, как вы используете UIAlertAction. Все, что вам нужно сделать, это импортировать AQMutiAlertFramework в свой проект, или вы можете также включить класс (см. Пример проекта для этого). Внутренне Он использует двоичный семафор для ошеломления предупреждений, пока пользователь не обработает действие, связанное с отображаемым текущим предупреждением. Сообщите мне, если он работает для вас.

Ответ 8

Я также столкнулся с такой же проблемой после переключения с UIAlertView на UIAlertController. Мне не нравится политика Apple, потому что "Message Boxes" всегда были стекированы почти в каждом SO из BIG BANG. Я согласен с тем, что одновременные оповещения не являются отличным пользовательским интерфейсом, а иногда и результатом плохого дизайна, но иногда (например, UILocalNotification или тому подобное) они просто могут случиться, и он испугался, что я могу потерять важное блокирующее предупреждение только потому, что мое приложение только что получил уведомление.

Тем не менее, это мое решение с двумя центрами, рекурсивная функция, которая пытается представить диспетчер alert на отправителе, если отправитель не имеет представленного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];
    }
}

Ответ 9

Я решил эту проблему с этой строкой кода:

alert.modalTransitionStyle=UIModalPresentationOverCurrentContext;