ビューコントローラーにないときにUIAlertControllerを提示する方法


255

シナリオ:ユーザーがビューコントローラーのボタンをタップします。ビューコントローラーは、ナビゲーションスタックの最上位(明らかに)です。タップは、別のクラスで呼び出されたユーティリティクラスメソッドを呼び出します。そこで悪いことが起こり、コントロールがビューコントローラーに戻る直前に警告を表示したいと思います。

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

これは可能UIAlertViewでした(しかし、おそらく適切ではありません)。

この場合、どのようにを提示UIAlertControllermyUtilityMethodますか?

回答:


34

数か月前に同様の質問を投稿しましたが、ようやく問題が解決したと思います。コードを見たいだけの場合は、投稿の下部にあるリンクをクリックしてください。

解決策は、追加のUIWindowを使用することです。

UIAlertControllerを表示する場合:

  1. ウィンドウをキーおよび表示ウィンドウにする(window.makeKeyAndVisible()
  2. 新しいウィンドウのrootViewControllerとしてプレーンなUIViewControllerインスタンスを使用するだけです。(window.rootViewController = UIViewController()
  3. ウィンドウのrootViewControllerにUIAlertControllerを表示します

注意すべき2つの点:

  • UIWindowを強く参照する必要があります。強く参照されていない場合は表示されません(リリースされているため)。プロパティの使用をお勧めしますが、関連付けられたオブジェクトでも成功しました。
  • ウィンドウが他のすべてのもの(システムUIAlertControllersを含む)の上に表示されるようにするために、windowLevelを設定します。(window.windowLevel = UIWindowLevelAlert + 1

最後に、もしあなたがそれを見たいだけなら、私は完成した実装を持っています。

https://github.com/dbettermann/DBAlertController


Objective-Cにはこれがありませんね。
SAHM

2
はい、それはSwift 2.0 / iOS 9でも機能します。他の誰かが要求したので(たぶんそれはあなただったので)、私は現在Objective-Cバージョンに取り組んでいます。完了したら、ポストバックします。
Dylan Bettermann 2015年

322

WWDCで、私はラボの1つに立ち寄り、アップルエンジニアに同じ質問をしましたUIAlertController。「?を表示するためのベストプラクティスは何でしたか?」そして、彼は彼らがこの質問をたくさん受けていたと言いました、そして私達は彼らがそれについてセッションを持っていたはずであると冗談を言った 彼は内部的にアップルがUIWindow透明なUIViewControllerを作成し、それを提示しUIAlertControllerていると述べました。基本的にディラン・ベターマンの答えには何がありますか。

しかしUIAlertController、アプリ全体でコードを変更する必要があるため、のサブクラスを使用したくありませんでした。そのため、関連付けられたオブジェクトの助けを借りて、Objective-CのメソッドUIAlertControllerを提供するカテゴリを作成しましたshow

関連するコードは次のとおりです。

#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ときに作成されることが破棄されますUIAlertControllerdeallocedされ、それが保持される唯一のオブジェクトであるため、UIWindow。ただし、をUIAlertControllerプロパティに割り当てるか、アクションブロックの1つでアラートにアクセスすることによって保持カウントを増加させると、UIWindowは画面上に留まり、UIがロックされます。にアクセスする必要がある場合に回避するには、上記のサンプル使用コードを参照してくださいUITextField

次のテストプロジェクトでGitHubリポジトリを作成しました:FFGlobalAlertController


1
いい物!ちょっとした背景-私はSwiftを使用していたので、関連付けられたオブジェクトの代わりにサブクラスを使用しました。関連オブジェクトはObjective-Cランタイムの機能であり、それに依存したくありませんでした。Swiftはおそらく独自のランタイムを取得するまでには数年かかりますが、それでもまだです。:)
ディランベターマン2015年

1
私はあなたの答えの優雅さが本当に好きですが、新しいウィンドウを閉じて元のウィンドウを再びキーにする方法に興味があります(確かに、ウィンドウをあまり操作していません)。
ダスティンPfannenstiel 2015年

1
キーウィンドウは一番上に表示されるウィンドウであるため、「キー」ウィンドウを削除/非表示にすると、次に表示されるウィンドウが「キー」になります。
agilityvision 2015年

19
viewDidDisappear:カテゴリでの実装は悪い考えのように見えます。本質的には、フレームワークのの実装と競合していますviewDidDisappear:。現時点では問題ないかもしれませんが、Appleが将来そのメソッドを実装することを決定した場合、それを呼び出す方法はありません(つまりsuper、カテゴリ実装からのメソッドの主要な実装を指す類似点はありません)。 。
adib

5
素晴らしい作品が、どのように治療することprefersStatusBarHiddenpreferredStatusBarStyle、余分なサブクラスなし?
Kevin Flachsmann 2016年

109

迅速

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

Objective-C

UIAlertController *alertController = [UIAlertController alertControllerWithTitle:@"Title" message:@"message" preferredStyle:UIAlertControllerStyleAlert];
//...
id rootViewController = [UIApplication sharedApplication].delegate.window.rootViewController;
if([rootViewController isKindOfClass:[UINavigationController class]])
{
    rootViewController = ((UINavigationController *)rootViewController).viewControllers.firstObject;
}
if([rootViewController isKindOfClass:[UITabBarController class]])
{
    rootViewController = ((UITabBarController *)rootViewController).selectedViewController;
}
//...
[rootViewController presentViewController:alertController animated:YES completion:nil];

2
+1これは見事にシンプルなソリューションです。(私が直面した問題:マスター/詳細テンプレートのDetailViewControllerにアラートを表示する-iPadでは表示されるが、iPhoneでは表示されない)
David

8
いいですね、別の部分を追加したいかもしれません:if(rootViewController.presentedViewController!= nil){rootViewController = rootViewController.presentedViewController; }
DivideByZer0

1
Swift 3: 'Alert'は 'alert'に名前変更されました:let alertController = UIAlertController(title: "title"、message: "message"、preferredStyle:.alert)
Kaptain

代わりにデリゲートを使用してください!
Andrew Kirna

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
おっと、確認する前に同意しました。このコードはルートビューコントローラーを返します。これは、私の場合はナビゲーションコントローラーです。エラーは発生しませんが、アラートは表示されません。
Murray Sagal 2014年

22
そして、私はコンソールで気づきました:Warning: Attempt to present <UIAlertController: 0x145bfa30> on <UINavigationController: 0x1458e450> whose view is not in the window hierarchy!
Murray Sagal 2014年

1
@MurraySagalにナビゲーションコントローラーがあると、visibleViewControllerいつでもプロパティを取得して、どのコントローラーからアラートを表示するかを確認できます。ドキュメントを
Lubo

2
他の人の作品の評価をしたくないので、私はそれをしました。@ZevEisenbergのソリューションで、Swift 3.0用に変更しました。私が別の答えを追加したとしたら、私は彼に値する賛成票を得たかもしれません。
jeet.chanchawat

1
ああ、昨日はすべてのドラマを見逃しましたが、たまたまSwift 3の投稿を更新しました。新しい言語バージョンの古い回答を更新する際のSOのポリシーはわかりませんが、個人的には気にしませんが、答えが正しい限り!
Zev Eisenberg

34

および/またはのUIAlertController extensionすべてのケースでかなり一般的です。画面上にモーダルVCがある場合にも機能します。 UINavigationControllerUITabBarController

使用法:

//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.currentVCselfviewDidAppearに、その後nilにしますviewWillDisappear。アプリ内のすべてのビューコントローラーはを継承しBaseViewControllerます。そうすることで、何かがある場合UI.currentVC(そうnilではありません...)-明らかにプレゼンテーションアニメーションの途中ではないため、にプレゼンテーションを依頼できますUIAlertController
Aviel Gross 2015

1
以下のように、ルートビューコントローラーはセグエで何かを提示している可能性があります。その場合、最後のifステートメントが失敗するため、追加する必要がありました else { if let presentedViewController = controller.presentedViewController { presentedViewController.presentViewController(self, animated: animated, completion: completion) } else { controller.presentViewController(self, animated: animated, completion: completion) } }
Niklas

27

agilityvisionの回答を改善するには、透明なルートビューコントローラーでウィンドウを作成し、そこからアラートビューを表示する必要があります。

ただし、アラートコントローラにアクションがある限り、ウィンドウへの参照を保持する必要はありません。アクションハンドラブロックの最後のステップとして、クリーンアップタスクの一部としてウィンドウを非表示にするだけです。ハンドラーブロックでウィンドウへの参照を持つことにより、アラートコントローラーが閉じられると壊れる一時的な循環参照が作成されます。

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

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

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

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

[window makeKeyAndVisible];
[window.rootViewController presentViewController:alertCtrl animated:YES completion:nil];

パーフェクト、正確に
言えば

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にアクセスできない場合でも使用できます。
Alfi

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
新しいウィンドウが閉じられなかったように聞こえます。
Igor Kulagin

ウィンドウが上から削除されないように見えるので、完了したらウィンドウを削除する必要があります。
soan saini

あなたの設定しalertWindowにはnil、あなたがそれで終わっているとき。
C6Silver

13

Zevの答えに加えて(そしてObjective-Cに切り替えて)、ルートビューコントローラーがセグエまたは他の何かを介して他のVCを提示している状況に遭遇する可能性があります。ルートVCでpresentViewControllerを呼び出すと、これが処理されます。

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

これにより、ルートVCが別のVCにセグメンテーションし、アラートコントローラーを表示する代わりに、上記のような警告が発行されたという問題が解決しました。

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

私はテストしていませんが、ルートVCがたまたまナビゲーションコントローラーである場合にも、これが必要になる場合があります。


私はSwiftでこの問題に遭遇していて、objcコードを迅速に変換する方法が見つからないので、助けていただければ幸いです!

2
@MayerzがObjective-CをSwiftに変換することはそれほど大きな問題ではないはずです;)しかし、ここにあります:UIApplication.sharedApplication().keyWindow?.rootViewController?.presentedViewController?.presentViewController(controller, animated: true, completion: nil)
borchero

オリビエに感謝します、そうです、それはパイのように簡単です、そして私はそれをこのように翻訳しました、しかし問題はどこか他のところにありました。とにかくありがとう!

Attempting to load the view of a view controller while it is deallocating is not allowed and may result in undefined behavior (<UIAlertController: 0x15cd4afe0>)
Mojo66 2016

2
私は同じアプローチで行きました、rootViewController.presentedViewControllerもしそれがnilでなければ、そうでなければを使いrootViewControllerます。完全に一般的なソリューションの場合presentedViewControllertopmostVC に到達するためにs のチェーンを歩く必要があるかもしれません
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.
注意してください

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

これらの2つのスレッドは重複としてフラグが付けられていないため、私の回答をクロスポストします...

これで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

Zev Eisenbergの答えは単純明快ですが、常に機能するとは限らず、次の警告メッセージで失敗することがあります。

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

これは、ウィンドウのrootViewControllerが表示されたビューの上部にないためです。これを修正するには、Swift 3で作成したUIAlertController拡張コードに示されているように、プレゼンテーションチェーンを上に移動する必要があります。

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

2017年9月15日の更新:

上記のロジックが新しく利用可能なiOS 11 GMシードで引き続き適切に機能することをテストして確認しました。ただし、アジリティビジョンで上位に投票されたメソッドはそうではありません。新しく作成されたアラートに表示されるアラートビューUIWindowはキーボードの下にあり、ユーザーがボタンをタップできない可能性があります。これは、iOS 11では、キーボードウィンドウよりも高いすべてのwindowLevelsがその下のレベルに引き下げられるためです。

keyWindowただし、警告が表示されるとキーボードが下にスライドし、警告が解除されるとキーボードが再び上にスライドするアニメーションが1つあります。プレゼンテーション中にキーボードをそのままにしたい場合は、以下のコードに示すように、トップウィンドウ自体からプレゼンテーションを試すことができます。

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シードで適切に機能し、適切な色合いで、キーボードのスライドアーティファクトがありません。


ここで以前の多くの回答を調べたところ、Kevin Sliechの回答が表示されました。これは、同様のアプローチで同じ問題を解決しようとしていますが、プレゼンテーションチェーンを上るのをやめるのではなく、解決しようとする同じエラーの影響を受けやすくなっています。 。
CodeBrew 2017

4

スウィフト4+

私が何年も使用して何の問題もない解決策。まず、UIWindowvisibleViewControllerを見つけるために拡張します。:カスタムコレクション*クラス(サイドメニューなど)を使用している場合は、次の拡張機能でこのケースのハンドラーを追加する必要があります。最上位のView Controllerを取得した後は、の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の場合、mythicalcoderbobbyrehmの回答に基づいて構築します。

iOS 13では、独自のウィンドウを作成してアラートを表示する場合、そのウィンドウへの強い参照を保持する必要があります。そうしないと、参照がスコープを出るとウィンドウがすぐに割り当て解除されるため、アラートは表示されません。

さらに、ウィンドウを削除して、その下のメインウィンドウでのユーザー操作を引き続き可能にするために、アラートが閉じられた後、参照をnilに再度設定する必要があります。

UIViewControllerウィンドウのメモリ管理ロジックをカプセル化するサブクラスを作成できます。

class WindowAlertPresentationController: UIViewController {

    // MARK: - Properties

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

    // MARK: - Initialization

    init(alert: UIAlertController) {

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

    required init?(coder aDecoder: NSCoder) {

        fatalError("This initializer is not supported")
    }

    // MARK: - Presentation

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

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

    // MARK: - Overrides

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

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

これをそのまま使用することも、で便利なメソッドが必要な場合UIAlertControllerは、拡張機能でスローすることもできます。

extension UIAlertController {

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

        let windowAlertPresentationController = WindowAlertPresentationController(alert: self)
        windowAlertPresentationController.present(animated: animated, completion: completion)
    }
}

アラートを手動で解除する必要がある場合、これは機能しません-WindowAlertPresentationControllerが割り当て解除されないため、UIがフリーズします
JBlake

アラートを手動でdismiss消したい場合は、必ずWindowAlertPresentationControllerを直接呼び出してください alert.presentingViewController?.dismiss(animated: true, completion: nil)
JBlake

let alertController = UIAlertController(title: "title"、message: "message"、preferredStyle:.alert); alertController.presentInOwnWindow(アニメーション:false、完了:nil)は私にとってはうまくいきます!ありがとう!
ブライアン、

これはiOS 12.4.5を搭載したiPhone 6では機能しますが、iOS 13.3.1を搭載したiPhone 11 Proでは機能しません。エラーはありませんが、アラートは表示されません。任意の提案をいただければ幸いです。
jl303

iOS 13に最適です。Catalystでは機能しません-アラートが解除されると、アプリは操作できなくなります。@Peter Lapisuのソリューションを参照してください
JBlake

3

Objective-Cでアラートを表示する簡単な方法:

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

オブジェクトalertControllerはどこにありますかUIAlertController

注:ヘルパークラスが拡張されていることを確認する必要もあります。 UIViewController


3

誰かが興味を持っている場合、@ agilityvision回答のSwift 3バージョンを作成しました。コード:

import Foundation
import UIKit

extension UIAlertController {

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

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

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

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

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

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

        self.window = window
    }
}

@Chathuranga:編集を元に戻しました。その「エラー処理」は完全に不要です。
マーティン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
        }
    }
}

これを使えば、アラートを簡単に提示できます

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

注意すべき点の1つは、現在表示されているUIAlertControllerがある場合、UIApplication.topMostViewControllerがを返すことUIAlertControllerです。上に表示するのUIAlertControllerは奇妙な動作であり、避ける必要があります。そのため、!(UIApplication.topMostViewController is UIAlertController)提示する前に手動でチェックするか、else ifケースを追加してnilを返す必要があります。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.
}

はい、それは可能であり、機能します。しかし、私にとっては、コードの匂いが少しあります。渡されたパラメータは通常、呼び出されたメソッドがその主要な機能を実行するために必要です。さらに、既存のすべての呼び出しを変更する必要があります。
Murray Sagal、2015

1

Kevin Sliechは素晴らしいソリューションを提供しました。

次に、メインのUIViewControllerサブクラスで以下のコードを使用します。

私が行った小さな変更の1つは、最適なプレゼンテーションコントローラがプレーンなUIViewControllerではないかどうかを確認することでした。そうでない場合は、プレーンVCを表すVCである必要があります。したがって、代わりに提示されているVCを返します。

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

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

    return bestPresentationController;
}

私のテストではこれまでのところすべてがうまくいったようです。

ケビンありがとう!


1

与えられた素晴らしい答えに加えて(敏捷性ビジョンadibmalhal)。古き良き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で機能します。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

AndのUIViewControllerような - (void)presentErrorMessage;メソッドでカテゴリを実装してみて、そのメソッド内でUIAlertControllerを実装してから、それをに表示することができselfます。クライアントコードよりも次のようになります。

[myViewController presentErrorMessage];

このようにして、ビューがウィンドウ階層に含まれていないという不必要なパラメーターや警告を回避できます。


myViewController悪いことをするコードに私がいないことを除いて。それは、それを呼び出したビューコントローラについて何も知らないユーティリティメソッドにあります。
Murray Sagal

2
IMHOがビュー(したがってアラート)をユーザーに提示するのはViewControllersの責任です。したがって、コードの一部がviewControllerについて何も認識していない場合、ユーザーにエラーを表示するのではなく、コードの「viewController aware」部分にエラーを渡します
Vlad Soroka

2
同意する。しかし、現在は非推奨となっUIAlertViewているため、いくつかの点でそのルールを破ることになりました。
Murray Sagal

0

使用できる2つの方法があります。

- UIAlertViewまたは「UIActionSheet」を代わりに使用してください(推奨されません。iOS8では非推奨になりますが、現在は機能しています)

-どういうわけか、表示される最後のビューコントローラを覚えています。ここに例があります。

@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);
    }
}
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.