Как представить UIAlertController, когда не в контроллере представления?

255

Сценарий: пользователь нажимает кнопку на контроллере представления. Контроллер представления является самым верхним (очевидно) в стеке навигации. Tap вызывает метод вспомогательного класса, вызываемый для другого класса. Там происходит что-то плохое, и я хочу отобразить предупреждение прямо перед тем, как управление вернется в контроллер представления.

+ (void)myUtilityMethod {
    // do stuff
    // something bad happened, display an alert.
}

Это было возможно с UIAlertView(но, возможно, не совсем правильно).

В этом случае, как вы представляете UIAlertController, прямо там myUtilityMethod?

Мюррей Сагал
источник

Ответы:

34

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

Решением является использование дополнительного UIWindow.

Когда вы хотите отобразить свой UIAlertController:

  1. Сделайте свое окно ключом и видимым окном ( window.makeKeyAndVisible())
  2. Просто используйте простой экземпляр UIViewController в качестве rootViewController нового окна. ( window.rootViewController = UIViewController())
  3. Представьте свой UIAlertController в rootViewController вашего окна

Несколько вещей, чтобы отметить:

  • На ваше UIWindow должна быть дана прямая ссылка. Если на него нет сильных ссылок, оно никогда не появится (потому что оно выпущено). Я рекомендую использовать свойство, но у меня также был успех со связанным объектом .
  • Чтобы убедиться, что окно отображается над всем остальным (включая системные UIAlertControllers), я установил windowLevel. ( window.windowLevel = UIWindowLevelAlert + 1)

Наконец, у меня есть завершенная реализация, если вы просто хотите посмотреть на это.

https://github.com/dbettermann/DBAlertController

Дилан Беттерманн
источник
У вас нет этого для Objective-C, не так ли?
SAHM
2
Да, это работает даже в Swift 2.0 / iOS 9. Сейчас я работаю над версией Objective-C, потому что кто-то еще просил об этом (возможно, это был ты). Я отправлю обратно, когда я закончу.
Дилан Беттерманн
322

На WWDC я зашел в одну из лабораторий и спросил у инженера Apple тот же вопрос: «Как лучше всего показывать UIAlertController?» И он сказал, что они часто получают этот вопрос, и мы пошутили, что у них должна была быть сессия по этому вопросу. Он сказал, что внутренне Apple создает UIWindowпрозрачный пакет UIViewControllerи затем представляет UIAlertControllerего. В основном то, что в ответе Дилана Беттермана.

Но я не хотел использовать подкласс, UIAlertControllerпотому что это потребовало бы от меня изменения кода во всем приложении. Итак, с помощью связанного объекта я создал категорию, UIAlertControllerкоторая предоставляет showметод в Objective-C.

Вот соответствующий код:

#import "UIAlertController+Window.h"
#import <objc/runtime.h>

@interface UIAlertController (Window)

- (void)show;
- (void)show:(BOOL)animated;

@end

@interface UIAlertController (Private)

@property (nonatomic, strong) UIWindow *alertWindow;

@end

@implementation UIAlertController (Private)

@dynamic alertWindow;

- (void)setAlertWindow:(UIWindow *)alertWindow {
    objc_setAssociatedObject(self, @selector(alertWindow), alertWindow, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

- (UIWindow *)alertWindow {
    return objc_getAssociatedObject(self, @selector(alertWindow));
}

@end

@implementation UIAlertController (Window)

- (void)show {
    [self show:YES];
}

- (void)show:(BOOL)animated {
    self.alertWindow = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
    self.alertWindow.rootViewController = [[UIViewController alloc] init];

    id<UIApplicationDelegate> delegate = [UIApplication sharedApplication].delegate;
    // Applications that does not load with UIMainStoryboardFile might not have a window property:
    if ([delegate respondsToSelector:@selector(window)]) {
        // we inherit the main window's tintColor
        self.alertWindow.tintColor = delegate.window.tintColor;
    }

    // window level is above the top window (this makes the alert, if it's a sheet, show over the keyboard)
    UIWindow *topWindow = [UIApplication sharedApplication].windows.lastObject;
    self.alertWindow.windowLevel = topWindow.windowLevel + 1;

    [self.alertWindow makeKeyAndVisible];
    [self.alertWindow.rootViewController presentViewController:self animated:animated completion:nil];
}

- (void)viewDidDisappear:(BOOL)animated {
    [super viewDidDisappear:animated];
    
    // precaution to ensure window gets destroyed
    self.alertWindow.hidden = YES;
    self.alertWindow = nil;
}

@end

Вот пример использования:

// need local variable for TextField to prevent retain cycle of Alert otherwise UIWindow
// would not disappear after the Alert was dismissed
__block UITextField *localTextField;
UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"Global Alert" message:@"Enter some text" preferredStyle:UIAlertControllerStyleAlert];
[alert addAction:[UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) {
    NSLog(@"do something with text:%@", localTextField.text);
// do NOT use alert.textfields or otherwise reference the alert in the block. Will cause retain cycle
}]];
[alert addTextFieldWithConfigurationHandler:^(UITextField *textField) {
    localTextField = textField;
}];
[alert show];

UIWindow, Который создается будет разрушен , когда UIAlertControllerэто dealloced, так как это единственный объект, сохраняя UIWindow. Но если вы присваиваете UIAlertControllerсвойству свойство или заставляете его увеличивать счет хранения, обращаясь к предупреждению в одном из блоков действий, оно UIWindowбудет оставаться на экране, блокируя ваш пользовательский интерфейс. См. Пример кода использования выше, чтобы избежать в случае необходимости доступа UITextField.

Я сделал репозиторий GitHub с тестовым проектом: FFGlobalAlertController

agilityvision
источник
1
Хорошая вещь! Просто некоторый фон - я использовал подкласс вместо связанного объекта, потому что я использовал Swift. Связанные объекты - это особенность среды выполнения Objective C, и я не хотел зависеть от нее. Свифту, вероятно, еще далеко до того, как он запустит собственную среду выполнения, но все же. :)
Дилан Беттерманн
1
Мне очень нравится элегантность вашего ответа, однако мне любопытно, как вы выберете новое окно и снова сделаете оригинальное окно ключом (по общему признанию, я не много слоняюсь с окном).
Дастин Пфанненштиэль
1
Ключевое окно - самое верхнее видимое окно, поэтому я понимаю, что если вы удалите / скроете окно «ключ», следующее видимое окно внизу станет «ключом».
agilityvision
19
Реализация viewDidDisappear:на категории выглядит как плохая идея. По сути, вы конкурируете с реализацией платформы viewDidDisappear:. Пока это может быть хорошо, но если Apple решит внедрить этот метод в будущем, у вас не будет возможности вызвать его (т. Е. Нет аналога тому, superчто указывает на первичную реализацию метода из реализации категории) ,
Адиб
5
Прекрасно работает, но как лечить prefersStatusBarHiddenи preferredStatusBarStyleбез лишних подклассов?
Кевин Флахсманн
109

стриж

let alertController = UIAlertController(title: "title", message: "message", preferredStyle: .alert)
//...
var rootViewController = UIApplication.shared.keyWindow?.rootViewController
if let navigationController = rootViewController as? UINavigationController {
    rootViewController = navigationController.viewControllers.first
}
if let tabBarController = rootViewController as? UITabBarController {
    rootViewController = tabBarController.selectedViewController
}
//...
rootViewController?.present(alertController, animated: true, completion: nil)

Objective-C

UIAlertController *alertController = [UIAlertController alertControllerWithTitle:@"Title" message:@"message" preferredStyle:UIAlertControllerStyleAlert];
//...
id rootViewController = [UIApplication sharedApplication].delegate.window.rootViewController;
if([rootViewController isKindOfClass:[UINavigationController class]])
{
    rootViewController = ((UINavigationController *)rootViewController).viewControllers.firstObject;
}
if([rootViewController isKindOfClass:[UITabBarController class]])
{
    rootViewController = ((UITabBarController *)rootViewController).selectedViewController;
}
//...
[rootViewController presentViewController:alertController animated:YES completion:nil];
Darkngs
источник
2
+1 Это блестяще простое решение. (Проблема, с которой я столкнулся: отображение предупреждения в DetailViewController шаблона Master / Detail - отображается на iPad, а не на iPhone)
David
8
Хорошо, вы можете добавить еще одну часть: if (rootViewController.presentedViewController! = Nil) {rootViewController = rootViewController.presentedViewController; }
DivideByZer0
1
Swift 3: «Оповещение» было переименовано в «alert»: let alertController = UIAlertController (title: «title», message: «message», предпочитаемый стиль: .alert)
Kaptain
Вместо этого используйте делегата!
Эндрю Кирна
104

Вы можете сделать следующее с Swift 2.2:

let alertController: UIAlertController = ...
UIApplication.sharedApplication().keyWindow?.rootViewController?.presentViewController(alertController, animated: true, completion: nil)

И Swift 3.0:

let alertController: UIAlertController = ...
UIApplication.shared.keyWindow?.rootViewController?.present(alertController, animated: true, completion: nil)
Зев Айзенберг
источник
12
К сожалению, я принял, прежде чем я проверил. Этот код возвращает корневой контроллер представления, который в моем случае является контроллером навигации. Это не вызывает ошибку, но предупреждение не отображается.
Мюррей Сагал
22
И я заметил , в консоли Warning: Attempt to present <UIAlertController: 0x145bfa30> on <UINavigationController: 0x1458e450> whose view is not in the window hierarchy!.
Мюррей Сагал
1
@MurraySagal, имея контроллер навигации, вы можете visibleViewControllerв любое время получить свойство, чтобы узнать, с какого контроллера выдается предупреждение. Проверьте документы
Lubo
2
Я сделал это, потому что я не хочу брать кредиты за чужую работу. Это было решение @ZevEisenberg, которое я модифицировал для swift 3.0. Если бы я добавил еще один ответ, я мог бы получить голоса, которые он заслуживает.
jeet.chanchawat
1
О, эй, я пропустил всю драму вчера, но я только что обновил пост для Swift 3. Я не знаю, какова политика SO по обновлению старых ответов для новых языковых версий, но я лично не против этого, до тех пор, пока ответ правильный!
Зев Айзенберг,
34

Довольно общий UIAlertController extensionдля всех случаев UINavigationControllerи / или UITabBarController. Также работает, если на экране есть модальный ВК.

Использование:

//option 1:
myAlertController.show()
//option 2:
myAlertController.present(animated: true) {
    //completion code...
}

Это расширение:

//Uses Swift1.2 syntax with the new if-let
// so it won't compile on a lower version.
extension UIAlertController {

    func show() {
        present(animated: true, completion: nil)
    }

    func present(#animated: Bool, completion: (() -> Void)?) {
        if let rootVC = UIApplication.sharedApplication().keyWindow?.rootViewController {
            presentFromController(rootVC, animated: animated, completion: completion)
        }
    }

    private func presentFromController(controller: UIViewController, animated: Bool, completion: (() -> Void)?) {
        if  let navVC = controller as? UINavigationController,
            let visibleVC = navVC.visibleViewController {
                presentFromController(visibleVC, animated: animated, completion: completion)
        } else {
          if  let tabVC = controller as? UITabBarController,
              let selectedVC = tabVC.selectedViewController {
                presentFromController(selectedVC, animated: animated, completion: completion)
          } else {
              controller.presentViewController(self, animated: animated, completion: completion)
          }
        }
    }
}
Авиэль Гросс
источник
1
Я использовал это решение, и я нашел его действительно идеальным, элегантным, чистым ... НО, недавно мне пришлось сменить корневой контроллер представления на представление, не входящее в иерархию представлений, поэтому этот код стал бесполезным. Кто-нибудь думает о Диксе, чтобы продолжать использовать это?
1
Я использую комбинацию этого решения с sometinhg еще: у меня есть одноэлементный UIкласс , который держит (слабый!) currentVCТипа UIViewController.I имеет BaseViewControllerкоторый наследует от UIViewControllerи набора UI.currentVCдля selfна viewDidAppearто , чтобы nilна viewWillDisappear. Все мои контроллеры представления в приложении наследуются BaseViewController. Таким образом, если у вас есть что-то UI.currentVC(это не nil...) - это определенно не в середине анимации презентации, и вы можете попросить ее представить свою UIAlertController.
Авиэль Гросс
1
Как показано ниже, корневой контроллер представления может представлять что-то с помощью segue, и в этом случае ваш последний оператор if не выполняется, поэтому мне пришлось добавить else { if let presentedViewController = controller.presentedViewController { presentedViewController.presentViewController(self, animated: animated, completion: completion) } else { controller.presentViewController(self, animated: animated, completion: completion) } }
Niklas
27

Улучшение на ответ agilityvision в , вам нужно создать окно с прозрачным контроллером зрения корневого и представить вид предупреждения оттуда.

Однако, пока у вас есть действие в контроллере оповещений, вам не нужно хранить ссылку на окно . В качестве последнего шага блока обработчика действий, вам просто нужно скрыть окно как часть задачи очистки. Имея ссылку на окно в блоке обработчика, это создает временную циклическую ссылку, которая будет разорвана, как только контроллер оповещения будет отклонен.

UIWindow* window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
window.rootViewController = [UIViewController new];
window.windowLevel = UIWindowLevelAlert + 1;

UIAlertController* alertCtrl = [UIAlertController alertControllerWithTitle:... message:... preferredStyle:UIAlertControllerStyleAlert];

[alertCtrl addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"OK",@"Generic confirm") style:UIAlertActionStyleCancel handler:^(UIAlertAction * _Nonnull action) {
    ... // do your stuff

    // very important to hide the window afterwards.
    // this also keeps a reference to the window until the action is invoked.
    window.hidden = YES;
}]];

[window makeKeyAndVisible];
[window.rootViewController presentViewController:alertCtrl animated:YES completion:nil];
Adib
источник
Отлично, именно тот совет, который мне нужен, чтобы закрыть окно, спасибо приятель
Тибо Ноа
25

Следующее решение не сработало, хотя оно выглядело многообещающе для всех версий. Это решение генерирует ПРЕДУПРЕЖДЕНИЕ .

Внимание! Попытайтесь представить, чей вид не находится в иерархии окон!

https://stackoverflow.com/a/34487871/2369867 => Тогда это выглядело многообещающе. Но это было не в Swift 3. Поэтому я отвечаю на это в Swift 3, и это не пример шаблона.

Это довольно полнофункциональный код сам по себе, когда вы вставляете в любую функцию.

Быстрый Swift 3 автономный код

let alertController = UIAlertController(title: "<your title>", message: "<your message>", preferredStyle: UIAlertControllerStyle.alert)
alertController.addAction(UIAlertAction(title: "Close", style: UIAlertActionStyle.cancel, handler: nil))

let alertWindow = UIWindow(frame: UIScreen.main.bounds)
alertWindow.rootViewController = UIViewController()
alertWindow.windowLevel = UIWindowLevelAlert + 1;
alertWindow.makeKeyAndVisible()
alertWindow.rootViewController?.present(alertController, animated: true, completion: nil)

Это проверенный и рабочий код в Swift 3.

mythicalcoder
источник
1
Этот код отлично работал для меня в контексте, когда в делегате приложения запускался UIAlertController в связи с проблемой миграции до того, как был загружен любой контроллер корневого представления. Работал отлично, предупреждений нет.
Дункан Бэббидж
3
Просто напоминание: вам нужно сохранить сильную ссылку на ваше UIWindowокно, иначе окно будет выпущено и исчезнет вскоре после выхода из области видимости.
Сирены
24

Вот ответ мифического кодера как расширение, протестированное и работающее в Swift 4:

extension UIAlertController {

    func presentInOwnWindow(animated: Bool, completion: (() -> Void)?) {
        let alertWindow = UIWindow(frame: UIScreen.main.bounds)
        alertWindow.rootViewController = UIViewController()
        alertWindow.windowLevel = UIWindowLevelAlert + 1;
        alertWindow.makeKeyAndVisible()
        alertWindow.rootViewController?.present(self, animated: animated, completion: completion)
    }

}

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

let alertController = UIAlertController(title: "<Alert Title>", message: "<Alert Message>", preferredStyle: .alert)
alertController.addAction(UIAlertAction(title: "Close", style: .cancel, handler: nil))
alertController.presentInOwnWindow(animated: true, completion: {
    print("completed")
})
bobbyrehm
источник
Это можно использовать, даже если sharedApplication недоступен!
Альфи
20

Это работает в Swift для обычных контроллеров представления, и даже если на экране есть контроллер навигации:

let alert = UIAlertController(...)

let alertWindow = UIWindow(frame: UIScreen.main.bounds)
alertWindow.rootViewController = UIViewController()
alertWindow.windowLevel = UIWindowLevelAlert + 1;
alertWindow.makeKeyAndVisible()
alertWindow.rootViewController?.presentViewController(alert, animated: true, completion: nil)
Уильям Энтрикен
источник
1
Когда я отклоняю предупреждение, UIWindowоно не отвечает. Что-то связанное с, windowLevelвероятно, Как я могу сделать это отзывчивым?
слайдер
1
Похоже, новое окно не было закрыто.
Игорь Кулагин
Похоже, окно не удаляется сверху, поэтому нужно удалить окно, как только сделано.
Соан Сайни
Установите alertWindowTo , nilкогда вы закончите с ним.
C6Silver
13

Добавив к ответу Зев (и переключившись обратно на Objective-C), вы можете столкнуться с ситуацией, когда ваш корневой контроллер представления представляет какой-то другой виртуальный канал через сеанс или что-то еще. Вызов представленногоViewController в корневом VC позаботится об этом:

[[UIApplication sharedApplication].keyWindow.rootViewController.presentedViewController presentViewController:alertController animated:YES completion:^{}];

Это устранило проблему, возникшую у меня, когда корневой VC перешел на другой VC, и вместо представления контроллера предупреждений было выдано предупреждение, подобное описанным выше:

Warning: Attempt to present <UIAlertController: 0x145bfa30> on <UINavigationController: 0x1458e450> whose view is not in the window hierarchy!

Я не проверял это, но это также может быть необходимо, если ваш корневой VC оказывается контроллером навигации.

Кевин Слих
источник
Хм, я сталкиваюсь с этой проблемой в Swift, и я не могу найти, как перевести ваш objc-код в swift, помощь будет принята с благодарностью!
2
@Mayerz перевод Objective-C на Swift не должен быть таким уж большим делом;) но вот вы:UIApplication.sharedApplication().keyWindow?.rootViewController?.presentedViewController?.presentViewController(controller, animated: true, completion: nil)
borchero
Спасибо Оливье, вы правы, это просто, как пирог, и я сделал это так, но проблема была в другом месте. Спасибо, в любом случае!
Attempting to load the view of a view controller while it is deallocating is not allowed and may result in undefined behavior (<UIAlertController: 0x15cd4afe0>)
Mojo66
2
Я пошел с тем же подходом, используйте, rootViewController.presentedViewControllerесли его не ноль, в противном случае, используя rootViewController. Для полностью универсального решения, возможно, потребуется пройтись по цепочке presentedViewControllers, чтобы попасть в topmostВК
Protongun
9

Ответ @ agilityvision переведен на Swift4 / iOS11. Я не использовал локализованные строки, но вы можете легко это изменить:

import UIKit

/** An alert controller that can be called without a view controller.
 Creates a blank view controller and presents itself over that
 **/
class AlertPlusViewController: UIAlertController {

    private var alertWindow: UIWindow?

    override func viewDidLoad() {
        super.viewDidLoad()
    }

    override func viewDidDisappear(_ animated: Bool) {
        super.viewDidDisappear(animated)
        self.alertWindow?.isHidden = true
        alertWindow = nil
    }

    func show() {
        self.showAnimated(animated: true)
    }

    func showAnimated(animated _: Bool) {

        let blankViewController = UIViewController()
        blankViewController.view.backgroundColor = UIColor.clear

        let window = UIWindow(frame: UIScreen.main.bounds)
        window.rootViewController = blankViewController
        window.backgroundColor = UIColor.clear
        window.windowLevel = UIWindowLevelAlert + 1
        window.makeKeyAndVisible()
        self.alertWindow = window

        blankViewController.present(self, animated: true, completion: nil)
    }

    func presentOkayAlertWithTitle(title: String?, message: String?) {

        let alertController = AlertPlusViewController(title: title, message: message, preferredStyle: .alert)
        let okayAction = UIAlertAction(title: "Ok", style: .default, handler: nil)
        alertController.addAction(okayAction)
        alertController.show()
    }

    func presentOkayAlertWithError(error: NSError?) {
        let title = "Error"
        let message = error?.localizedDescription
        presentOkayAlertWithTitle(title: title, message: message)
    }
}
Дилан Колако
источник
Я получал черный фон с принятым ответом. window.backgroundColor = UIColor.clearисправил это. viewController.view.backgroundColor = UIColor.clearне кажется необходимым.
Бен Патч
Имейте в виду, что Apple предупреждает о UIAlertControllerThe UIAlertController class is intended to be used as-is and does not support subclassing. The view hierarchy for this class is private and must not be modified.
создании
6

Создайте расширение как в ответе Aviel Gross. Здесь у вас есть расширение Objective-C.

Здесь у вас есть заголовочный файл * .h

//  UIAlertController+Showable.h

#import <UIKit/UIKit.h>

@interface UIAlertController (Showable)

- (void)show;

- (void)presentAnimated:(BOOL)animated
             completion:(void (^)(void))completion;

- (void)presentFromController:(UIViewController *)viewController
                     animated:(BOOL)animated
                   completion:(void (^)(void))completion;

@end

И реализация: * .m

//  UIAlertController+Showable.m

#import "UIAlertController+Showable.h"

@implementation UIAlertController (Showable)

- (void)show
{
    [self presentAnimated:YES completion:nil];
}

- (void)presentAnimated:(BOOL)animated
             completion:(void (^)(void))completion
{
    UIViewController *rootVC = [UIApplication sharedApplication].keyWindow.rootViewController;
    if (rootVC != nil) {
        [self presentFromController:rootVC animated:animated completion:completion];
    }
}

- (void)presentFromController:(UIViewController *)viewController
                     animated:(BOOL)animated
                   completion:(void (^)(void))completion
{

    if ([viewController isKindOfClass:[UINavigationController class]]) {
        UIViewController *visibleVC = ((UINavigationController *)viewController).visibleViewController;
        [self presentFromController:visibleVC animated:animated completion:completion];
    } else if ([viewController isKindOfClass:[UITabBarController class]]) {
        UIViewController *selectedVC = ((UITabBarController *)viewController).selectedViewController;
        [self presentFromController:selectedVC animated:animated completion:completion];
    } else {
        [viewController presentViewController:self animated:animated completion:completion];
    }
}

@end

Вы используете это расширение в вашем файле реализации следующим образом:

#import "UIAlertController+Showable.h"

UIAlertController* alert = [UIAlertController
    alertControllerWithTitle:@"Title here"
                     message:@"Detail message here"
              preferredStyle:UIAlertControllerStyleAlert];

UIAlertAction* defaultAction = [UIAlertAction
    actionWithTitle:@"OK"
              style:UIAlertActionStyleDefault
            handler:^(UIAlertAction * action) {}];
[alert addAction:defaultAction];

// Add more actions if needed

[alert show];
Марчин Капуста
источник
4

Перекрестите мой ответ, так как эти две темы не помечены как обманщики ...

Теперь это UIViewControllerчасть цепочки респондента, вы можете сделать что-то вроде этого:

if let vc = self.nextResponder()?.targetForAction(#selector(UIViewController.presentViewController(_:animated:completion:)), withSender: self) as? UIViewController {

    let alert = UIAlertController(title: "A snappy title", message: "Something bad happened", preferredStyle: .Alert)
    alert.addAction(UIAlertAction(title: "OK", style: .Default, handler: nil))

    vc.presentViewController(alert, animated: true, completion: nil)
}
Марк Ауффлик
источник
4

Зев Айзенберг ответ прост и прямолинеен, но он не всегда работает, и он может потерпеть неудачу с этим предупреждением:

Warning: Attempt to present <UIAlertController: 0x7fe6fd951e10>  
 on <ThisViewController: 0x7fe6fb409480> which is already presenting 
 <AnotherViewController: 0x7fe6fd109c00>

Это связано с тем, что rootViewController для Windows находится не в верхней части представленных представлений. Чтобы исправить это, нам нужно пройтись по цепочке презентаций, как показано в моем коде расширения UIAlertController, написанном в Swift 3:

   /// show the alert in a view controller if specified; otherwise show from window's root pree
func show(inViewController: UIViewController?) {
    if let vc = inViewController {
        vc.present(self, animated: true, completion: nil)
    } else {
        // find the root, then walk up the chain
        var viewController = UIApplication.shared.keyWindow?.rootViewController
        var presentedVC = viewController?.presentedViewController
        while presentedVC != nil {
            viewController = presentedVC
            presentedVC = viewController?.presentedViewController
        }
        // now we present
        viewController?.present(self, animated: true, completion: nil)
    }
}

func show() {
    show(inViewController: nil)
}

Обновления от 15.09.2017:

Протестировано и подтверждено, что вышеуказанная логика все еще отлично работает в недавно появившемся семени iOS 11 GM. Однако метод agilityvision, получивший наибольшее количество голосов, не делает этого: представление предупреждений, представленное недавно отчеканенным, UIWindowнаходится под клавиатурой и потенциально не позволяет пользователю нажимать на его кнопки. Это связано с тем, что в iOS 11 все уровни окна выше, чем у окна клавиатуры, опускаются до уровня ниже него.

Одним из артефактов представления из-за этого keyWindowявляется анимация, когда клавиатура скользит вниз при появлении предупреждения и снова сдвигается вверх при отклонении предупреждения. Если вы хотите, чтобы клавиатура оставалась там во время презентации, вы можете попытаться представить ее из самого верхнего окна, как показано в приведенном ниже коде:

func show(inViewController: UIViewController?) {
    if let vc = inViewController {
        vc.present(self, animated: true, completion: nil)
    } else {
        // get a "solid" window with the highest level
        let alertWindow = UIApplication.shared.windows.filter { $0.tintColor != nil || $0.className() == "UIRemoteKeyboardWindow" }.sorted(by: { (w1, w2) -> Bool in
            return w1.windowLevel < w2.windowLevel
        }).last
        // save the top window's tint color
        let savedTintColor = alertWindow?.tintColor
        alertWindow?.tintColor = UIApplication.shared.keyWindow?.tintColor

        // walk up the presentation tree
        var viewController = alertWindow?.rootViewController
        while viewController?.presentedViewController != nil {
            viewController = viewController?.presentedViewController
        }

        viewController?.present(self, animated: true, completion: nil)
        // restore the top window's tint color
        if let tintColor = savedTintColor {
            alertWindow?.tintColor = tintColor
        }
    }
}

Единственная не столь значительная часть приведенного выше кода состоит в том, что он проверяет имя класса, UIRemoteKeyboardWindowчтобы убедиться, что мы тоже можем его включить. Тем не менее приведенный выше код отлично работает в iOS 9, 10 и 11 GM seed, с правильным оттенком цвета и без артефактов скольжения клавиатуры.

CodeBrew
источник
Просто просмотрел множество предыдущих ответов здесь и увидел ответ Кевина Слиха, который пытается решить ту же проблему с помощью аналогичного подхода, но не останавливается на ходу цепочки презентаций, что делает его подверженным той же ошибке, что и пытается решить ,
CodeBrew
4

Свифт 4+

Решение я использую годами без каких-либо проблем. Прежде всего, я расширяю, UIWindowчтобы найти его visibleViewController. ПРИМЕЧАНИЕ : если вы используете пользовательские классы коллекции * (например, боковое меню), вы должны добавить обработчик для этого случая в следующем расширении. После того, как вы получили самый верхний вид контроллера, его легко представить UIAlertControllerкак UIAlertView.

extension UIAlertController {

  func show(animated: Bool = true, completion: (() -> Void)? = nil) {
    if let visibleViewController = UIApplication.shared.keyWindow?.visibleViewController {
      visibleViewController.present(self, animated: animated, completion: completion)
    }
  }

}

extension UIWindow {

  var visibleViewController: UIViewController? {
    guard let rootViewController = rootViewController else {
      return nil
    }
    return visibleViewController(for: rootViewController)
  }

  private func visibleViewController(for controller: UIViewController) -> UIViewController {
    var nextOnStackViewController: UIViewController? = nil
    if let presented = controller.presentedViewController {
      nextOnStackViewController = presented
    } else if let navigationController = controller as? UINavigationController,
      let visible = navigationController.visibleViewController {
      nextOnStackViewController = visible
    } else if let tabBarController = controller as? UITabBarController,
      let visible = (tabBarController.selectedViewController ??
        tabBarController.presentedViewController) {
      nextOnStackViewController = visible
    }

    if let nextOnStackViewController = nextOnStackViewController {
      return visibleViewController(for: nextOnStackViewController)
    } else {
      return controller
    }
  }

}
Тимур Берникович
источник
4

Для iOS 13, опираясь на ответы мифического кодера и bobbyrehm :

В iOS 13, если вы создаете свое собственное окно для представления оповещения, вы должны иметь четкую ссылку на это окно, иначе ваше оповещение не будет отображаться, потому что окно будет немедленно освобождено, когда его ссылка выходит из области действия.

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

Вы можете создать UIViewControllerподкласс для инкапсуляции логики управления памятью окна:

class WindowAlertPresentationController: UIViewController {

    // MARK: - Properties

    private lazy var window: UIWindow? = UIWindow(frame: UIScreen.main.bounds)
    private let alert: UIAlertController

    // MARK: - Initialization

    init(alert: UIAlertController) {

        self.alert = alert
        super.init(nibName: nil, bundle: nil)
    }

    required init?(coder aDecoder: NSCoder) {

        fatalError("This initializer is not supported")
    }

    // MARK: - Presentation

    func present(animated: Bool, completion: (() -> Void)?) {

        window?.rootViewController = self
        window?.windowLevel = UIWindow.Level.alert + 1
        window?.makeKeyAndVisible()
        present(alert, animated: animated, completion: completion)
    }

    // MARK: - Overrides

    override func dismiss(animated flag: Bool, completion: (() -> Void)? = nil) {

        super.dismiss(animated: flag) {
            self.window = nil
            completion?()
        }
    }
}

Вы можете использовать это как есть, или, если вам нужен удобный метод UIAlertController, вы можете добавить его в расширение:

extension UIAlertController {

    func presentInOwnWindow(animated: Bool, completion: (() -> Void)?) {

        let windowAlertPresentationController = WindowAlertPresentationController(alert: self)
        windowAlertPresentationController.present(animated: animated, completion: completion)
    }
}
Логан Готье
источник
Это не работает, если вам нужно отключить предупреждение вручную - WindowAlertPresentationController никогда не отменяется, что приводит к зависанию пользовательского интерфейса - ничто не является интерактивным из-за того, что окно все еще там
JBlake
Если вы хотите dismissотключить предупреждение вручную, обязательно вызовите WindowAlertPresentationController напрямую alert.presentingViewController?.dismiss(animated: true, completion: nil)
JBlake
let alertController = UIAlertController (title: "title", message: "message", предпочитаемый стиль: .alert); alertController.presentInOwnWindow (animated: false, завершение: nil) прекрасно работает для меня! Спасибо!
Брайан
Это работает на iPhone 6 с iOS 12.4.5, но не на iPhone 11 Pro с iOS 13.3.1. Там нет ошибки, но предупреждение никогда не отображается. Любое предложение будет оценено.
jl303
Отлично работает для iOS 13. Не работает в Catalyst - после того, как оповещение отключено, приложение перестает взаимодействовать. Посмотреть решение @Peter Lapisu
JBlake
3

Сокращенный способ сделать предупреждение в Objective-C:

[[[[UIApplication sharedApplication] keyWindow] rootViewController] presentViewController:alertController animated:YES completion:nil];

Где alertControllerтвой UIAlertControllerобъект

ПРИМЕЧАНИЕ. Вам также необходимо убедиться, что ваш вспомогательный класс расширяется. UIViewController

ViperMav
источник
3

Если кому-то интересно, я создал версию @agilityvision Swift 3. Код:

import Foundation
import UIKit

extension UIAlertController {

    var window: UIWindow? {
        get {
            return objc_getAssociatedObject(self, "window") as? UIWindow
        }
        set {
            objc_setAssociatedObject(self, "window", newValue, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC)
        }
    }

    open override func viewDidDisappear(_ animated: Bool) {
        super.viewDidDisappear(animated)
        self.window?.isHidden = true
        self.window = nil
    }

    func show(animated: Bool = true) {
        let window = UIWindow(frame: UIScreen.main.bounds)
        window.rootViewController = UIViewController(nibName: nil, bundle: nil)

        let delegate = UIApplication.shared.delegate
        if delegate?.window != nil {
            window.tintColor = delegate!.window!!.tintColor
        }

        window.windowLevel = UIApplication.shared.windows.last!.windowLevel + 1

        window.makeKeyAndVisible()
        window.rootViewController!.present(self, animated: animated, completion: nil)

        self.window = window
    }
}
Майстер
источник
@Chathuranga: я отменил ваше редактирование. Такая «обработка ошибок» совершенно не нужна.
Мартин Р
2
extension UIApplication {
    /// The top most view controller
    static var topMostViewController: UIViewController? {
        return UIApplication.shared.keyWindow?.rootViewController?.visibleViewController
    }
}

extension UIViewController {
    /// The visible view controller from a given view controller
    var visibleViewController: UIViewController? {
        if let navigationController = self as? UINavigationController {
            return navigationController.topViewController?.visibleViewController
        } else if let tabBarController = self as? UITabBarController {
            return tabBarController.selectedViewController?.visibleViewController
        } else if let presentedViewController = presentedViewController {
            return presentedViewController.visibleViewController
        } else {
            return self
        }
    }
}

С этим вы можете легко представить свое предупреждение, как так

UIApplication.topMostViewController?.present(viewController, animated: true, completion: nil)

Стоит отметить, что если в данный момент отображается UIAlertController, UIApplication.topMostViewControllerон вернет a UIAlertController. Представление сверху UIAlertControllerимеет странное поведение и его следует избегать. Таким образом, вы должны либо вручную проверить это !(UIApplication.topMostViewController is UIAlertController)перед представлением, либо добавить else ifслучай, чтобы вернуть ноль, еслиself is UIAlertController

extension UIViewController {
    /// The visible view controller from a given view controller
    var visibleViewController: UIViewController? {
        if let navigationController = self as? UINavigationController {
            return navigationController.topViewController?.visibleViewController
        } else if let tabBarController = self as? UITabBarController {
            return tabBarController.selectedViewController?.visibleViewController
        } else if let presentedViewController = presentedViewController {
            return presentedViewController.visibleViewController
        } else if self is UIAlertController {
            return nil
        } else {
            return self
        }
    }
}
NSExceptional
источник
1

Вы можете отправить текущий вид или контроллер в качестве параметра:

+ (void)myUtilityMethod:(id)controller {
    // do stuff
    // something bad happened, display an alert.
}
Пабло А.
источник
Да, это возможно и будет работать. Но для меня это немного запаха кода. Передаваемые параметры должны обычно требоваться для вызываемого метода для выполнения его основной функции. Кроме того, все существующие вызовы должны быть изменены.
Мюррей Сагал
1

Кевин Слих предоставил отличное решение.

Теперь я использую приведенный ниже код в моем основном подклассе UIViewController.

Я сделал небольшое изменение, чтобы проверить, не является ли лучший контроллер представления простым UIViewController. Если нет, то это должен быть какой-то ВК, представляющий простой ВК. Таким образом мы возвращаем VC, который представлен вместо этого.

- (UIViewController *)bestPresentationController
{
    UIViewController *bestPresentationController = [UIApplication sharedApplication].keyWindow.rootViewController;

    if (![bestPresentationController isMemberOfClass:[UIViewController class]])
    {
        bestPresentationController = bestPresentationController.presentedViewController;
    }    

    return bestPresentationController;
}

Кажется, все получилось в моем тестировании.

Спасибо Кевин!

Андрей
источник
1

В дополнение к отличным ответам ( agilityvision , adib , malhal ). Чтобы достичь поведения в очереди, как в старых добрых UIAlertViews (избегайте перекрытия окон предупреждений), используйте этот блок для наблюдения за доступностью на уровне окна:

@interface UIWindow (WLWindowLevel)

+ (void)notifyWindowLevelIsAvailable:(UIWindowLevel)level withBlock:(void (^)())block;

@end

@implementation UIWindow (WLWindowLevel)

+ (void)notifyWindowLevelIsAvailable:(UIWindowLevel)level withBlock:(void (^)())block {
    UIWindow *keyWindow = [UIApplication sharedApplication].keyWindow;
    if (keyWindow.windowLevel == level) {
        // window level is occupied, listen for windows to hide
        id observer;
        observer = [[NSNotificationCenter defaultCenter] addObserverForName:UIWindowDidBecomeHiddenNotification object:keyWindow queue:nil usingBlock:^(NSNotification *note) {
            [[NSNotificationCenter defaultCenter] removeObserver:observer];
            [self notifyWindowLevelIsAvailable:level withBlock:block]; // recursive retry
        }];

    } else {
        block(); // window level is available
    }
}

@end

Полный пример:

[UIWindow notifyWindowLevelIsAvailable:UIWindowLevelAlert withBlock:^{
    UIWindow *alertWindow = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
    alertWindow.windowLevel = UIWindowLevelAlert;
    alertWindow.rootViewController = [UIViewController new];
    [alertWindow makeKeyAndVisible];

    UIAlertController *alertController = [UIAlertController alertControllerWithTitle:@"Alert" message:nil preferredStyle:UIAlertControllerStyleAlert];
    [alertController addAction:[UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleCancel handler:^(UIAlertAction *action) {
        alertWindow.hidden = YES;
    }]];

    [alertWindow.rootViewController presentViewController:alertController animated:YES completion:nil];
}];

Это позволит вам избежать перекрытия окон предупреждений. Тот же метод можно использовать для разделения и помещения контроллеров представления очереди для любого числа слоев окна.

Роман Б.
источник
1

Я перепробовал все упомянутое, но безуспешно. Метод, который я использовал для Swift 3.0:

extension UIAlertController {
    func show() {
        present(animated: true, completion: nil)
    }

    func present(animated: Bool, completion: (() -> Void)?) {
        if var topController = UIApplication.shared.keyWindow?.rootViewController {
            while let presentedViewController = topController.presentedViewController {
                topController = presentedViewController
            }
            topController.present(self, animated: animated, completion: completion)
        }
    }
}
Dragisa Dragisic
источник
1

Некоторые из этих ответов работали только частично для меня, объединение их в следующем методе класса в AppDelegate было для меня решением. Он работает на iPad, в представлениях UITabBarController, в UINavigationController, en, когда представляет модалы. Протестировано на iOS 10 и 13.

+ (UIViewController *)rootViewController {
    UIViewController *rootViewController = [UIApplication sharedApplication].delegate.window.rootViewController;
    if([rootViewController isKindOfClass:[UINavigationController class]])
        rootViewController = ((UINavigationController *)rootViewController).viewControllers.firstObject;
    if([rootViewController isKindOfClass:[UITabBarController class]])
        rootViewController = ((UITabBarController *)rootViewController).selectedViewController;
    if (rootViewController.presentedViewController != nil)
        rootViewController = rootViewController.presentedViewController;
    return rootViewController;
}

Использование:

[[AppDelegate rootViewController] presentViewController ...
Eerko
источник
1

Поддержка сцены iOS13 (при использовании UIWindowScene)

import UIKit

private var windows: [String:UIWindow] = [:]

extension UIWindowScene {
    static var focused: UIWindowScene? {
        return UIApplication.shared.connectedScenes
            .first { $0.activationState == .foregroundActive && $0 is UIWindowScene } as? UIWindowScene
    }
}

class StyledAlertController: UIAlertController {

    var wid: String?

    func present(animated: Bool, completion: (() -> Void)?) {

        //let window = UIWindow(frame: UIScreen.main.bounds)
        guard let window = UIWindowScene.focused.map(UIWindow.init(windowScene:)) else {
            return
        }
        window.rootViewController = UIViewController()
        window.windowLevel = .alert + 1
        window.makeKeyAndVisible()
        window.rootViewController!.present(self, animated: animated, completion: completion)

        wid = UUID().uuidString
        windows[wid!] = window
    }

    open override func viewDidDisappear(_ animated: Bool) {
        super.viewDidDisappear(animated)
        if let wid = wid {
            windows[wid] = nil
        }

    }

}
Питер Лапису
источник
UIAlerController не должен быть разделен на подклассы в соответствии с документацией developer.apple.com/documentation/uikit/uialertcontroller
отзывы
0

Вы можете попытаться реализовать категорию UIViewControllerс помощью метода, такого как - (void)presentErrorMessage;А, и внутри этого метода вы реализуете UIAlertController, а затем представите его self. Чем в вашем клиентском коде у вас будет что-то вроде:

[myViewController presentErrorMessage];

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

Влад Сорока
источник
За исключением того, что у меня нет myViewControllerв коде, где случается плохое. Это в служебном методе, который ничего не знает о контроллере вида, который его вызвал.
Мюррей Сагал
2
IMHO, представление любых представлений (таким образом, оповещений) пользователю является обязанностью ViewControllers. Поэтому, если какая-то часть кода ничего не знает о viewController, она не должна сообщать пользователю об ошибках, а скорее передавать их «частям кода, осведомленным о viewController»
Влад Сорока,
2
Я согласен. Но удобство ныне устаревшего UIAlertViewпривело меня к нарушению этого правила в нескольких местах.
Мюррей Сагал
0

Есть 2 подхода, которые вы можете использовать:

-Используйте UIAlertViewили «UIActionSheet» вместо этого (не рекомендуется, потому что он устарел в iOS 8, но теперь работает)

Как-то помните последний представленный контроллер представления. Вот пример.

@interface UIViewController (TopController)
+ (UIViewController *)topViewController;
@end

// implementation

#import "UIViewController+TopController.h"
#import <objc/runtime.h>

static __weak UIViewController *_topViewController = nil;

@implementation UIViewController (TopController)

+ (UIViewController *)topViewController {
    UIViewController *vc = _topViewController;
    while (vc.parentViewController) {
        vc = vc.parentViewController;
    }
    return vc;
}

+ (void)load {
    [super load];
    [self swizzleSelector:@selector(viewDidAppear:) withSelector:@selector(myViewDidAppear:)];
    [self swizzleSelector:@selector(viewWillDisappear:) withSelector:@selector(myViewWillDisappear:)];
}

- (void)myViewDidAppear:(BOOL)animated {
    if (_topViewController == nil) {
        _topViewController = self;
    }

    [self myViewDidAppear:animated];
}

- (void)myViewWillDisappear:(BOOL)animated {
    if (_topViewController == self) {
        _topViewController = nil;
    }

    [self myViewWillDisappear:animated];
}

+ (void)swizzleSelector:(SEL)sel1 withSelector:(SEL)sel2
{
    Class class = [self class];

    Method originalMethod = class_getInstanceMethod(class, sel1);
    Method swizzledMethod = class_getInstanceMethod(class, sel2);

    BOOL didAddMethod = class_addMethod(class,
                                        sel1,
                                        method_getImplementation(swizzledMethod),
                                        method_getTypeEncoding(swizzledMethod));

    if (didAddMethod) {
        class_replaceMethod(class,
                            sel2,
                            method_getImplementation(originalMethod),
                            method_getTypeEncoding(originalMethod));
    } else {
        method_exchangeImplementations(originalMethod, swizzledMethod);
    }
}

@end 

Использование:

[[UIViewController topViewController] presentViewController:alertController ...];
Gralex
источник
0

Я использую этот код с небольшими изменениями в своем классе AppDelegate

-(UIViewController*)presentingRootViewController
{
    UIViewController *vc = self.window.rootViewController;
    if ([vc isKindOfClass:[UINavigationController class]] ||
        [vc isKindOfClass:[UITabBarController class]])
    {
        // filter nav controller
        vc = [AppDelegate findChildThatIsNotNavController:vc];
        // filter tab controller
        if ([vc isKindOfClass:[UITabBarController class]]) {
            UITabBarController *tbc = ((UITabBarController*)vc);
            if ([tbc viewControllers].count > 0) {
                vc = [tbc viewControllers][tbc.selectedIndex];
                // filter nav controller again
                vc = [AppDelegate findChildThatIsNotNavController:vc];
            }
        }
    }
    return vc;
}
/**
 *   Private helper
 */
+(UIViewController*)findChildThatIsNotNavController:(UIViewController*)vc
{
    if ([vc isKindOfClass:[UINavigationController class]]) {
        if (((UINavigationController *)vc).viewControllers.count > 0) {
            vc = [((UINavigationController *)vc).viewControllers objectAtIndex:0];
        }
    }
    return vc;
}
Sound Blaster
источник
0

Кажется, работает:

static UIViewController *viewControllerForView(UIView *view) {
    UIResponder *responder = view;
    do {
        responder = [responder nextResponder];
    }
    while (responder && ![responder isKindOfClass:[UIViewController class]]);
    return (UIViewController *)responder;
}

-(void)showActionSheet {
    UIAlertController *alertController = [UIAlertController alertControllerWithTitle:nil message:nil preferredStyle:UIAlertControllerStyleActionSheet];
    [alertController addAction:[UIAlertAction actionWithTitle:@"Cancel" style:UIAlertActionStyleCancel handler:nil]];
    [alertController addAction:[UIAlertAction actionWithTitle:@"Do it" style:UIAlertActionStyleDefault handler:nil]];
    [viewControllerForView(self) presentViewController:alertController animated:YES completion:nil];
}
wonder.mice
источник
0

создать вспомогательный класс AlertWindow и затем использовать как

let alertWindow = AlertWindow();
let alert = UIAlertController(title: "Hello", message: "message", preferredStyle: .alert);
let cancel = UIAlertAction(title: "Ok", style: .cancel){(action) in

    //....  action code here

    // reference to alertWindow retain it. Every action must have this at end

    alertWindow.isHidden = true;

   //  here AlertWindow.deinit{  }

}
alert.addAction(cancel);
alertWindow.present(alert, animated: true, completion: nil)


class AlertWindow:UIWindow{

    convenience init(){
        self.init(frame:UIScreen.main.bounds);
    }

    override init(frame: CGRect) {
        super.init(frame: frame);
        if let color = UIApplication.shared.delegate?.window??.tintColor {
            tintColor = color;
        }
        rootViewController = UIViewController()
        windowLevel = UIWindowLevelAlert + 1;
        makeKeyAndVisible()
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    deinit{
        //  semaphor.signal();
    }

    func present(_ ctrl:UIViewController, animated:Bool, completion: (()->Void)?){
        rootViewController!.present(ctrl, animated: animated, completion: completion);
    }
}
john07
источник