Як представити UIAlertController, коли він не знаходиться в контролері перегляду?


255

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

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

Це було можливо за допомогою UIAlertView(але, можливо, не зовсім правильно).

У цьому випадку, як ви представляєте UIAlertController, прямо там myUtilityMethod?

Відповіді:


34

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

Рішення полягає у використанні додаткового вікна UIW.

Коли ви бажаєте відобразити свій UIAlertController:

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

Кілька речей, які слід зазначити:

  • На ваше вікно UIW має бути посилання. Якщо на нього немає посилань, він ніколи не з’явиться (тому що він випущений). Я рекомендую використовувати властивість, але я також мав успіх із пов’язаним об’єктом .
  • Для того, щоб вікно з’явилося вище всього іншого (включаючи системну 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


1
Хороший матеріал! Просто деякий фон - я використовував підклас замість асоційованого об'єкта, оскільки я використовував Swift. Асоційовані об'єкти - це особливість виконання Objective-C, і я не хотів бути залежним від нього. Свіфт, мабуть, років далеко від отримання власного часу виконання, але все ж. :)
Ділан Беттерманн

1
Мені дуже подобається елегантність вашої відповіді, однак мені цікаво, як ви відстороните нове вікно і знову зробите оригінальне вікно ключем (правда, я не дуже сильно з цим вікном збиваюся).
Дастін Пфанненштьєль

1
Ключове вікно - це найвище видиме вікно, тому, наскільки я розумію, якщо ви видалите / приховете вікно "ключ", наступне видиме вікно вниз стає "ключовим".
спритність

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)

Ціль-С

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

2
+1 Це геніально просте рішення. (Проблема, з якою я стикався: Відображення попередження у шаблоні Master / Detail - DetailViewController - Показує на iPad, ніколи на iPhone)
Девід

8
Добре, ви можете додати в іншій частині: якщо (rootViewController.presentedViewController! = Нуль) {rootViewController = rootViewController.presentedViewController; }
DivideByZer0

1
Swift 3: "Попередження" було перейменовано на "сигнал сповіщення": нехай alarController = UIAlertController (назва: "заголовок", повідомлення: "повідомлення", кращийStyle: .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в будь-який час отримати майно, щоб побачити, з якого контролера подати сповіщення. Ознайомтеся з документами
Любо

2
Я це зробив, бо не хочу брати кредити на чужу роботу. Це рішення @ZevEisenberg я змінив для швидкого 3.0. Якби я додав ще одну відповідь, то, можливо, я отримав би голоси, які він заслуговує.
jeet.chanchawat

1
О, так, я пропустив всю драму вчора, але, здається, якраз оновив пост для Swift 3. Я не знаю, яка політика SO щодо оновлення старих відповідей на нові мовні версії, але я особисто не проти, до тих пір, поки відповідь правильна!
Zev Eisenberg

34

Досить загальне UIAlertController extensionдля всіх випадків UINavigationControllerта / або UITabBarController. Також працює, якщо в даний момент на екрані є модальний VC.

Використання:

//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.
Aviel Gross

1
Як зазначено нижче, контролер кореневого перегляду може щось представляти з segue, і в цьому випадку ваше останнє, якщо заява не вдасться, тому мені довелося додати else { if let presentedViewController = controller.presentedViewController { presentedViewController.presentViewController(self, animated: animated, completion: completion) } else { controller.presentViewController(self, animated: animated, completion: completion) } }
Niklas

27

Поліпшення відповіді на спритність , вам потрібно буде створити вікно з прозорим контролером кореневого перегляду та подати звідти вигляд попередження.

Однак , поки у вас є дії в контролері попередження, вам не потрібно зберігати посилання на вікно . На завершальному етапі блоку обробника дій вам просто потрібно сховати вікно як частину завдання очищення. Маючи посилання на вікно в блоці обробника, це створює тимчасову циркулярну посилання, яка буде порушена після відключення контролера попередження.

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

Ідеально, саме підказки, які мені знадобилися, щоб закрити вікно, спасибі товариш
Thibaut Noah

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.


1
Цей код відмінно працював для мене в контексті, коли в Делегат додатків було знято UIAlertController щодо проблеми міграції, перш ніж будь-який контролер кореневого перегляду був завантажений. Працювали чудово, ніяких попереджень.
Дункан Беббідж

3
Лише нагадування: вам потрібно зберегти чітке посилання на своє, UIWindowінакше вікно буде випущене і зникне незабаром після виходу із сфери застосування.
Сирени

24

Ось відповідь mythicalcoder як розширення, перевірене та працююче у 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")
})

Це можна використовувати навіть у тому випадку, якщо 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
Здається, що нове вікно не було відхилено.
Ігор Кулагін

Схоже, вікно не видаляється зверху, тому потрібно видалити вікно після завершення.
soan saini

Встановіть alertWindowTo , nilколи ви закінчите з ним.
C6Silver

13

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

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

Це випрямило проблему, у якій виник корінний ПК на інший VC, і замість того, щоб подати контролер сповіщення, було видано попередження, як повідомлялося вище:

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

Я не перевіряв це, але це також може знадобитися, якщо ваш кореневий VC є контролером навігації.


Hum Я зіткнувся з цією проблемою в Swift, і я не знаходжу, як перекласти ваш код objc на швидкий, допомогу буде дуже вдячний!

2
@Mayerz переклад "Objective-C" на "Свіфт" не повинен бути такою великою справою;), але ось ви: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, щоб дістатися до topmostVC
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 попереджає про UIAlertControllerпідкласифікацію: The 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. developer.apple.com/documentation/uikit/uialertcontroller
Grubas

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>

Це відбувається тому, що Windows rootViewController не знаходиться вгорі представлених представлень. Щоб виправити це, нам потрібно пройти ланцюжок презентацій, як показано в моєму коді розширення 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. Однак метод голосування, проведений методом спритності, не робить: огляд попередження, представлений у щойно відрізаномуUIWindow знаходиться нижче клавіатури і, можливо, не дозволяє користувачеві натискати його кнопки. Це тому, що в iOS 11 всі windowLevels вище, ніж у вікні клавіатури, опускаються до рівня нижче нього.

Одним з артефактів подання від 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 насінні, з правильним кольором відтінку і без розсувних артефактів клавіатури.


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

4

Швидкий 4+

Рішення, яке я використовую роками, без проблем. Перш за все я розширюю, UIWindowщоб знайти його видимимViewController. ПРИМІТКА . Якщо ви використовуєте власні колекції * класи (наприклад, бічне меню), слід додати обробник для цього випадку в наступному розширенні. Отримавши найкращий контролер перегляду, його легко представити 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, спираючись на відповіді mythicalcoder та bobbyrehm :

У iOS 13, якщо ви створюєте власне вікно для подання попередження, вам потрібно мати чітке посилання на це вікно, інакше ваше попередження не відображатиметься, оскільки вікно буде негайно розміщено, коли його посилання вийде із сфери застосування.

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

Ви можете створити 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

нехай alertController = UIAlertController (назва: "назва", повідомлення: "повідомлення", кращийStyle: .alert); alertController.presentInOwnWindow (анімований: помилковий, завершення: нульовий) працює для мене чудово! Дякую!
Брайан

Це працює на 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


3

Якщо когось цікавить, я створив Swift 3 версію відповіді @agilityvision. Код:

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повернеться 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
        }
    }
}

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

Окрім чудових відповідей ( спритність , адиб , мальхал ). Щоб дійти до поведінки в черзі, як у старих добрих 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)
        }
    }
}

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 ...

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
        }

    }

}

0

Ви можете спробувати реалізувати категорію UIViewControllerз таким - (void)presentErrorMessage;методом, як І, і всередині цього методу ви реалізуєте UIAlertController, а потім представити його self. Тож у вашому клієнтському коді у вас буде щось на кшталт:

[myViewController presentErrorMessage];

Таким чином ви уникнете непотрібних параметрів та попереджень про те, що перегляд не знаходиться у ієрархії вікон.


За винятком того, що я не маю myViewControllerв коді, де трапляється погане. Це в корисному методі, який нічого не знає про контролер перегляду, який його викликав.
Мюррей Сагал

2
IMHO, який представляє будь-які погляди (таким чином, сповіщення) користувачеві, є відповідальністю ViewControllers. Отже, якщо якась частина коду нічого не знає про viewController, вона не повинна представляти користувачам жодних помилок, а лише передавати їх частинам коду "viewController"
Влад Сорока

2
Я згоден. Але зручність застарілого тепер UIAlertViewзмусила мене порушити це правило в кількох місцях.
Мюррей Сагал

0

Є два підходи, якими ви можете скористатися:

-Використовуйте 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 ...];

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

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

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);
    }
}
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.