Como apresentar o UIAlertController quando não está em um controlador de exibição?

255

Cenário: O usuário toca em um botão em um controlador de exibição. O controlador de exibição é o mais alto (obviamente) na pilha de navegação. O toque chama um método de classe de utilitário chamado em outra classe. Acontece uma coisa ruim e desejo exibir um alerta antes que o controle retorne ao controlador de exibição.

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

Isso foi possível com UIAlertView(mas talvez não seja o bastante).

Nesse caso, como você apresenta um UIAlertController, ali mesmo myUtilityMethod?

Murray Sagal
fonte

Respostas:

34

Publiquei uma pergunta semelhante há alguns meses e acho que finalmente resolvi o problema. Siga o link na parte inferior da minha postagem, se você quiser apenas ver o código.

A solução é usar um UIWindow adicional.

Quando você deseja exibir seu UIAlertController:

  1. Faça da sua janela a janela principal e visível ( window.makeKeyAndVisible())
  2. Basta usar uma instância simples de UIViewController como rootViewController da nova janela. ( window.rootViewController = UIViewController())
  3. Apresente seu UIAlertController no rootViewController da sua janela

Algumas coisas a serem observadas:

  • Sua UIWindow deve ser fortemente referenciada. Se não for fortemente referenciado, nunca aparecerá (porque é lançado). Eu recomendo usar uma propriedade, mas também tive sucesso com um objeto associado .
  • Para garantir que a janela apareça acima de todo o resto (incluindo o sistema UIAlertControllers), defina o windowLevel. ( window.windowLevel = UIWindowLevelAlert + 1)

Por fim, tenho uma implementação concluída, se você quiser apenas ver isso.

https://github.com/dbettermann/DBAlertController

Dylan Bettermann
fonte
Você não tem isso para o Objective-C, tem?
SAHM
2
Sim, ele ainda funciona no Swift 2.0 / iOS 9. Estou trabalhando em uma versão do Objective-C agora porque outra pessoa pediu (talvez seja você). Vou postar de volta quando terminar.
Dylan Bettermann
322

Na WWDC, parei em um dos laboratórios e fiz a um engenheiro da Apple a mesma pergunta: "Qual era a melhor prática para exibir um UIAlertController?" E ele disse que eles estavam recebendo muito essa pergunta e brincamos que eles deveriam ter tido uma sessão nela. Ele disse que internamente a Apple está criando um UIWindowcom um transparente UIViewControllere, em seguida, apresentando o UIAlertControllernele. Basicamente, o que está na resposta de Dylan Betterman.

Mas eu não queria usar uma subclasse de, UIAlertControllerporque isso exigiria que eu mudasse meu código em todo o aplicativo. Então, com a ajuda de um objeto associado, criei uma categoria UIAlertControllerque fornece umashow método em Objective-C.

Aqui está o código relevante:

#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

Aqui está um exemplo de uso:

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

O UIWindowque é criado será destruído quando UIAlertControllerfor desalocado, pois é o único objeto que está retendo o UIWindow. Mas se você atribuir a UIAlertControlleruma propriedade ou aumentar sua contagem de retenção acessando o alerta em um dos blocos de ação, UIWindowele permanecerá na tela, bloqueando sua interface do usuário. Consulte o código de uso de amostra acima para evitar no caso de precisar acessarUITextField .

Fiz um repositório do GitHub com um projeto de teste: FFGlobalAlertController

visão de agilidade
fonte
1
Coisa boa! Apenas alguns antecedentes - usei uma subclasse em vez de um objeto associado porque estava usando o Swift. Objetos associados são um recurso do tempo de execução Objective-C e eu não queria depender disso. O Swift provavelmente está a anos de tempo de obter seu próprio tempo de execução, mas ainda assim. :)
Dylan Bettermann
1
Eu realmente gosto da elegância da sua resposta, no entanto, estou curioso para saber como você se aposenta a nova janela e transforma a janela original novamente em chave (é certo que eu não mexo muito com a janela).
Dustin Pfannenstiel
1
A janela principal é a janela visível superior, portanto, se você remover / ocultar a janela "chave", a próxima janela visível abaixo se tornará "chave".
agilityvision
19
Implementar viewDidDisappear:em uma categoria parece uma Má Idéia. Em essência, você está competindo com a implementação do framework viewDidDisappear:. Por enquanto, pode estar tudo bem, mas se a Apple decidir implementar esse método no futuro, não há como chamá-lo (ou seja, não há analogia superdisso que aponte para a implementação primária de um método a partir de uma implementação de categoria) .
adib
5
Funciona muito bem, mas como tratar prefersStatusBarHiddene preferredStatusBarStylesem uma subclasse extra?
Kevin Flachsmann
109

Rápido

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)

Objetivo-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
fonte
2
+1 Esta é uma solução brilhantemente simples. (Problema que eu enfrentei: Exibindo um alerta na DetailViewController de Master / modelo Detalhe - Shows no iPad, não no iPhone)
David
8
É bom adicionar outra parte: if (rootViewController.presentedViewController! = Zero) {rootViewController = rootViewController.presentedViewController; }
DivideByZer0 25/05
1
Swift 3: 'Alert' foi renomeado para 'alert': deixe alertController = UIAlertController (title: "title", message: "message", preferidoStyle: .alert)
Kaptain
Use um delegado!
Andrew Kirna 16/05/19
104

Você pode fazer o seguinte com o Swift 2.2:

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

E Swift 3.0:

let alertController: UIAlertController = ...
UIApplication.shared.keyWindow?.rootViewController?.present(alertController, animated: true, completion: nil)
Zev Eisenberg
fonte
12
Opa, aceitei antes de verificar. Esse código retorna o controlador de visualização raiz, que no meu caso é o controlador de navegação. Não causa um erro, mas o alerta não é exibido.
Murray Sagal 24/10
22
E eu notei no console: Warning: Attempt to present <UIAlertController: 0x145bfa30> on <UINavigationController: 0x1458e450> whose view is not in the window hierarchy!.
Murray Sagal 24/10
1
@MurraySagal com um controlador de navegação, você pode obter a visibleViewControllerpropriedade a qualquer momento para ver de qual controlador apresentar o alerta. Confira os documentos
Lubo
2
Fiz isso porque não quero receber créditos do trabalho de outra pessoa. Foi a solução do @ZevEisenberg que eu modifiquei para o Swift 3.0. Se eu tivesse acrescentado outra resposta, poderia ter votações que ele merece.
jeet.chanchawat
1
Oh, ei, eu perdi todo o drama de ontem, mas por acaso acabei de atualizar o post para o Swift 3. Não sei qual é a política da SO em atualizar respostas antigas para novas versões de idiomas, mas pessoalmente não me importo, contanto que a resposta esteja correta!
Zev Eisenberg
34

Bastante genérico UIAlertController extensionpara todos os casos de UINavigationControllere / ou UITabBarController. Também funciona se houver um VC modal na tela no momento.

Uso:

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

Esta é a extensão:

//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)
          }
        }
    }
}
Aviel Gross
fonte
1
Eu estava usando esta solução e achei realmente perfeita, elegante, limpa ... MAS, recentemente, tive que mudar meu controlador de exibição raiz para uma exibição que não estava na hierarquia da exibição, para que esse código se tornasse inútil. Alguém está pensando em um dix para continuar usando isso?
1
Eu uso uma combinação desta solução com sometinhg outra coisa: Eu tenho um singleton UIclasse que detém um (fraco!) currentVCDo tipo UIViewController.Eu tenho BaseViewControllerque herda de UIViewControllere conjunto UI.currentVCpara selfem viewDidAppearseguida, para nilon viewWillDisappear. Todos os meus controladores de exibição no aplicativo são herdados BaseViewController. Dessa forma, se você tem algo UI.currentVC(não é nil...) - ele definitivamente não está no meio de uma animação de apresentação e você pode pedir para apresentar sua UIAlertController.
Aviel Gross
1
Como por abaixo, o controlador de vista raiz pode apresentar algo com um segue, caso em que a sua última declaração se falhar, então eu tive que adicionar else { if let presentedViewController = controller.presentedViewController { presentedViewController.presentViewController(self, animated: animated, completion: completion) } else { controller.presentViewController(self, animated: animated, completion: completion) } }
Niklas
27

Para melhorar a resposta da agilityvision , você precisará criar uma janela com um controlador de visualização raiz transparente e apresentar a visualização de alerta a partir daí.

No entanto , desde que você tenha uma ação no seu controlador de alerta, não precisará manter uma referência à janela . Como etapa final do bloco manipulador de ação, você só precisa ocultar a janela como parte da tarefa de limpeza. Por ter uma referência à janela no bloco manipulador, isso cria uma referência circular temporária que seria interrompida quando o controlador de alerta fosse descartado.

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
fonte
Perfeito, exatamente a ponta que eu precisava para fechar a janela, graças acasalar
thibaut noah
25

A solução a seguir não funcionou, embora parecesse bastante promissora em todas as versões. Esta solução está gerando AVISO .

Aviso: Tente apresentar em cuja exibição não está na hierarquia da janela!

https://stackoverflow.com/a/34487871/2369867 => Isso parece promissor na época. Mas era não no Swift 3. Então, eu estou respondendo isso no Swift 3 e este não é um exemplo de modelo.

Este é um código totalmente funcional por si só, quando você cola dentro de qualquer função.

Código rápido e Swift 3 independente

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)

Este é um código testado e funcionando no Swift 3.

mythicalcoder
fonte
1
Esse código funcionou perfeitamente para mim, em um contexto em que um UIAlertController estava sendo acionado no App Delegate com relação a um problema de migração, antes de qualquer controlador de exibição raiz ter sido carregado. Funcionou muito bem, sem avisos.
Duncan Babbage
3
Apenas um lembrete: você precisa armazenar uma referência forte ao seu, caso UIWindowcontrário a janela será liberada e desaparecerá logo após sair do escopo.
Sirenes
24

Aqui está a resposta do mythicalcoder como uma extensão, testada e funcionando no 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)
    }

}

Exemplo de uso:

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
fonte
Isso pode ser usado mesmo se sharedApplication não estiver acessível!
Alfi 25/10
20

Isso funciona no Swift para controladores de exibição normais e mesmo se houver um controlador de navegação na tela:

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)
William Entriken
fonte
1
Quando eu rejeito o alerta, ele UIWindownão responde. Algo a ver com o windowLevelprovável. Como posso torná-lo responsivo?
Slider
1
Parece que a nova janela não foi descartada.
Igor Kulagin
Parece que a janela não é removida da parte superior, portanto, é necessário remover a janela depois de concluída.
soan saini
Defina seu alertWindowcomo nilquando terminar.
C6Silver 30/11/19
13

Adicionando à resposta de Zev (e retornando ao Objective-C), você pode se deparar com uma situação em que seu controlador de exibição raiz está apresentando outro VC por meio de um segue ou qualquer outra coisa. Chamar o PresentationViewController no VC raiz cuidará disso:

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

Isso solucionou um problema que tive onde o VC raiz seguiu para outro VC e, em vez de apresentar o controlador de alerta, um aviso como os relatados acima foi emitido:

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

Não testei, mas isso também pode ser necessário se o seu VC raiz for um controlador de navegação.

Kevin Sliech
fonte
Hum, estou enfrentando esse problema no Swift, e não encontro como traduzir seu código objc para swift, a ajuda seria muito apreciada!
2
@Mayerz traduzindo Objective-C para Swift não deve ser grande coisa;) mas aqui está você:UIApplication.sharedApplication().keyWindow?.rootViewController?.presentedViewController?.presentViewController(controller, animated: true, completion: nil)
borchero
Obrigado Olivier, você está certo, é fácil como torta, e eu traduzi dessa maneira, mas o problema estava em outro lugar. Obrigado mesmo assim!
Attempting to load the view of a view controller while it is deallocating is not allowed and may result in undefined behavior (<UIAlertController: 0x15cd4afe0>)
precisa
2
Eu fui com a mesma abordagem, use o rootViewController.presentedViewControllerse não for nulo, caso contrário, usando rootViewController. Para uma solução totalmente genérico, pode ser necessário andar a cadeia de presentedViewControllers para chegar ao topmostVC
Protongun
9

A resposta de @ agilityvision traduzida para Swift4 / iOS11. Não usei cadeias localizadas, mas você pode alterar isso facilmente:

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)
    }
}
Dylan Colaco
fonte
Eu estava recebendo um fundo preto com a resposta aceita. window.backgroundColor = UIColor.clearconsertou isso. viewController.view.backgroundColor = UIColor.clearnão parece ser necessário.
23418 Ben Patch
Tenha em mente que a Apple adverte sobre UIAlertControllersubclasses: 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

Crie uma extensão como na resposta Aviel Gross. Aqui você tem a extensão Objective-C.

Aqui você tem o arquivo de cabeçalho * .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

E implementação: * .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

Você está usando esta extensão em Seu arquivo de implementação, assim:

#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];
Marcin Kapusta
fonte
4

Post cruzado minha resposta pois esses dois tópicos não são sinalizados como dupes ...

Agora que UIViewControllerfaz parte da cadeia de respostas, você pode fazer algo assim:

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)
}
Mark Aufflick
fonte
4

A resposta de Zev Eisenberg é simples e direta, mas nem sempre funciona, e pode falhar com esta mensagem de aviso:

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

Isso ocorre porque o rootViewController do Windows não está no topo das visualizações apresentadas. Para corrigir isso, precisamos percorrer a cadeia de apresentação, conforme mostrado no meu código de extensão UIAlertController escrito em 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)
}

Atualizações em 15/09/2017:

Testou e confirmou que a lógica acima ainda funciona muito bem na nova semente iOS 11 GM disponível. O método mais votado pela agilityvision, no entanto, não: a exibição de alerta apresentada em umUIWindow fica abaixo do teclado e potencialmente impede que o usuário toque em seus botões. Isso ocorre porque no iOS 11 todos os níveis da janela mais altos que os da janela do teclado são reduzidos para um nível abaixo dele.

Um artefato de apresentação do keyWindowporém é a animação do teclado deslizando para baixo quando o alerta é apresentado e deslizando novamente para cima quando o alerta é descartado. Se você deseja que o teclado permaneça lá durante a apresentação, tente apresentar a partir da janela superior, como mostra o código abaixo:

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

A única parte não tão boa do código acima é que ele verifica o nome da classe UIRemoteKeyboardWindowpara garantir que também possamos incluí-lo. No entanto, o código acima funciona muito bem nas sementes GM iOS 9, 10 e 11 GM, com a cor certa e sem os artefatos deslizantes do teclado.

CodeBrew
fonte
Acabei de analisar as muitas respostas anteriores aqui e vi a resposta de Kevin Sliech, que está tentando resolver o mesmo problema com uma abordagem semelhante, mas que parou de subir na cadeia de apresentações, tornando-a suscetível ao mesmo erro que tenta resolver .
CodeBrew 9/08/17
4

Swift 4+

Solução que uso há anos sem problemas. Primeiro, estendo UIWindowpara descobrir que é visibleViewController. NOTA : se você estiver usando classes de coleção * personalizadas (como o menu lateral), adicione manipulador para este caso na seguinte extensão. Depois de obter o controle da maioria das visualizações, é fácil apresentar UIAlertControllerda mesma forma 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
    }
  }

}
Timur Bernikovich
fonte
4

Para o iOS 13, com base nas respostas de mythicalcoder e bobbyrehm :

No iOS 13, se você estiver criando sua própria janela para apresentar o alerta, será necessário manter uma forte referência a essa janela ou o alerta não será exibido porque a janela será imediatamente desalocada quando a referência sair do escopo.

Além disso, você precisará definir a referência como nulo novamente depois que o alerta for descartado para remover a janela e continuar permitindo a interação do usuário na janela principal abaixo dela.

Você pode criar uma UIViewControllersubclasse para encapsular a lógica de gerenciamento de memória da janela:

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

Você pode usá-lo como está ou, se quiser um método de conveniência UIAlertController, pode jogá-lo em uma extensão:

extension UIAlertController {

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

        let windowAlertPresentationController = WindowAlertPresentationController(alert: self)
        windowAlertPresentationController.present(animated: animated, completion: completion)
    }
}
Logan Gauthier
fonte
Isso não funciona se você precisa descartar o alerta manualmente - o WindowAlertPresentationController nunca é de-alocados, resultando em uma UI congelado - nada é interativo devido à janela ainda está lá
JBlake
Se você deseja descartar o alerta manualmente, chame dismisso WindowAlertPresentationController diretamente alert.presentingViewController?.dismiss(animated: true, completion: nil)
JBlake
deixe alertController = UIAlertController (title: "title", message: "message", preferidoStyle: .alert); alertController.presentInOwnWindow (animado: false, conclusão: nil) funciona muito bem para mim! Obrigado!
22719 Brian
Isso funciona no iPhone 6 com iOS 12.4.5, mas não no iPhone 11 Pro com iOS 13.3.1. Não há erro, mas o alerta nunca é exibido. Qualquer sugestão será apreciada.
jl303 9/02
Funciona muito bem para o iOS 13. Não funciona no Catalyst - depois que o alerta é descartado, o aplicativo não pode interagir. Veja a solução de
Peter Lapisu
3

Maneira abreviada de apresentar o alerta no Objective-C:

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

Onde alertControllerestá o seu UIAlertControllerobjeto?

NOTA: Você também precisará garantir que sua classe auxiliar estenda UIViewController

ViperMav
fonte
3

Se alguém estiver interessado, criei uma versão Swift 3 da resposta @agilityvision. O código:

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
    }
}
Majster
fonte
@Chathuranga: Eu reverti sua edição. Esse "tratamento de erros" é completamente desnecessário.
Martin R
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
        }
    }
}

Com isso, você pode facilmente apresentar seu alerta assim

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

Uma coisa a notar é que, se houver um UIAlertController atualmente sendo exibido, UIApplication.topMostViewControllerretornará a UIAlertController. Apresentar em cima de um UIAlertControllercomportamento estranho e deve ser evitado. Como tal, você deve verificar manualmente isso !(UIApplication.topMostViewController is UIAlertController)antes de apresentar ou adicionar um else ifcaso para retornar nulo seself 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
fonte
1

Você pode enviar a visualização atual ou o controlador como um parâmetro:

+ (void)myUtilityMethod:(id)controller {
    // do stuff
    // something bad happened, display an alert.
}
Pablo A.
fonte
Sim, isso é possível e funcionaria. Mas para mim, tem um pouco de cheiro de código. Os parâmetros passados ​​geralmente devem ser necessários para que o método chamado execute sua função principal. Além disso, todas as chamadas existentes precisariam ser modificadas.
Murray Sagal
1

Kevin Sliech forneceu uma ótima solução.

Agora uso o código abaixo na minha subclasse principal UIViewController.

Uma pequena alteração que fiz foi verificar se o melhor controlador de apresentação não é um UIViewController simples. Caso contrário, deve haver algum VC que apresente um VC simples. Assim, retornamos o VC que está sendo apresentado.

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

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

    return bestPresentationController;
}

Parece que tudo funcionou até agora nos meus testes.

Obrigado Kevin!

Andrew
fonte
1

Além de ótimas respostas dadas ( visão de agilidade , adib , malhal ). Para alcançar o comportamento de enfileiramento, como nos bons e antigos UIAlertViews (evitar a sobreposição de janelas de alerta), use este bloco para observar a disponibilidade no nível da janela:

@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

Exemplo completo:

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

Isso permitirá que você evite a sobreposição de janelas de alerta. O mesmo método pode ser usado para separar e colocar em controladores de exibição de fila para qualquer número de camadas de janela.

Roman B.
fonte
1

Eu tentei de tudo mencionado, mas sem sucesso. O método que eu usei para o 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
fonte
1

Algumas dessas respostas funcionaram apenas parcialmente para mim, combinando-as no método de classe a seguir no AppDelegate foi a solução para mim. Ele funciona no iPad, nas visualizações UITabBarController, no UINavigationController, e na apresentação de modais. Testado no iOS 10 e 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;
}

Uso:

[[AppDelegate rootViewController] presentViewController ...
Eerko
fonte
1

Suporte a cena iOS13 (ao usar o 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
        }

    }

}
Peter Lapisu
fonte
O UIAlerController não deve ser subclasse de acordo com a documentação developer.apple.com/documentation/uikit/uialertcontroller
accfews
0

Você pode tentar implementar uma categoria UIViewControllercom mehtod como - (void)presentErrorMessage;And e dentro desse método, você implementa o UIAlertController e, em seguida, apresenta-o self. Em seu código de cliente, você terá algo como:

[myViewController presentErrorMessage];

Dessa forma, você evitará parâmetros e avisos desnecessários sobre a exibição não estar na hierarquia da janela.

Vlad Soroka
fonte
Só que não tenho myViewControllerno código onde a coisa ruim acontece. Esse é um método utilitário que não sabe nada sobre o controlador de exibição que o chamou.
Murray Sagal
2
O IMHO que apresentar quaisquer pontos de vista (assim, alertas) ao usuário é de responsabilidade do ViewControllers. Então, se alguma parte do código não sabe nada sobre viewController não deve apresentar quaisquer erros para o usuário, mas sim passá-las para "viewcontroller conscientes" partes do código
Vlad Soroka
2
Concordo. Mas a conveniência do agora reprovado UIAlertViewme levou a quebrar essa regra em alguns pontos.
Murray Sagal
0

Existem 2 abordagens que você pode usar:

-Usar UIAlertView ou 'UIActionSheet' em vez (não recomendado, porque é preterido no iOS 8 mas funciona agora)

De alguma forma, lembre-se do último controlador de exibição que é apresentado. Aqui está um exemplo.

@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 

Uso:

[[UIViewController topViewController] presentViewController:alertController ...];
Gralex
fonte
0

Eu uso esse código com algumas pequenas variações pessoais na minha classe 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
fonte
0

Parece funcionar:

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
fonte
0

criar a classe auxiliar AlertWindow e depois usá-la como

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
fonte