iOS8でUIAlertViewからUIAlertControllerへ置き換えるのは大変だ

More than 3 years have passed since last update.


はじめに

iOS8でUIAlertViewが非推奨となり、UIAlertControllerへ置き換える作業を行っていたのですが、簡単にホイホイと置き換えられない事が判ってきましたので、その辺を備忘録的に書き留めておきたいと思います。


  • Xcode 6.0.1 + iOS8

  • iOS7以前の互換性は考慮していません。


  • UIAlertControllerの基本的な使い方は他の方の記事を参考にしてください。


表示元のUIViewControllerのインスタンスが必要になった

UIAlertViewを使って以下のような感じのお手軽アラート関数を作って、単純なアラートを表示させていた方もいらっしゃるかと思います。


UIAlertViewを使った例

// 警告を表示します。OKボタンタップで閉じます。

void Warning(NSString *message) {
[[[UIAlertView alloc] initWithTitle:@"" message:message delegate:nil cancelButtonTitle:@"OK" otherButtonTitles:nil] show];
}

この関数は、表示元情報を全く利用していませんので、UIViewControllerとは関係の無いクラスの中から直接呼び出してアラートを表示させる事も可能でした(行儀が良いかは置いておいて)。

しかし、UIAlertControllerUIViewControllerの派生クラスであり、presentViewController:animated:completion:を使って表示させるため、表示元のUIViewControllerのインスタンスが必要になります。

とは言え、先のWarning関数のようなお手軽アラートも作りたい!...ということで、適当に考えて実装してみました。


UIAlertControllerを使った例

// 警告を表示します。OKボタンタップで閉じます。

void Warning(NSString *message)
{
UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"" message:message preferredStyle:UIAlertControllerStyleAlert];
[alert addAction:[UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleDefault handler:nil]];

// 親ビューコンをなんとか検索
UIViewController *baseView = [UIApplication sharedApplication].keyWindow.rootViewController;
while (baseView.presentedViewController != nil && !baseView.presentedViewController.isBeingDismissed) {
baseView = baseView.presentedViewController;
}
[baseView presentViewController:alert animated:YES completion:nil];
}


一番手前のビューコンを探してくるだけというシンプルなものですが、UINavigationControllerなどが間に挟まっていても一応大丈夫なようです(完璧かどうかは判りません)。


二重でアラートを表示させる事ができなくなった

こういう実装をあえてする人はいないと思いますが、たまたま今まで動いていた場合、置き換えると動かなくなるというお話です。

(何かユーザーに複数のアラートを別のソース箇所から出している場合なんかが当てはまるかも?)


UIAlertViewを使った例

    [[[UIAlertView alloc] initWithTitle:@"" message:@"一個目" delegate:nil cancelButtonTitle:@"OK" otherButtonTitles:nil] show];

[[[UIAlertView alloc] initWithTitle:@"" message:@"二個目" delegate:nil cancelButtonTitle:@"OK" otherButtonTitles:nil] show];

上記の場合、「二個目」のアラートが手前に表示されて、OKボタンをタップすると「一個目」のアラートが後から出てきます。

同じような事をUIAlertControllerでやろうとすると...


UIAlertControllerを使った例

    {

UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"" message:@"一個目" preferredStyle:UIAlertControllerStyleAlert];
[alert addAction:[UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleDefault handler:nil]];
[self presentViewController:alert animated:YES completion:nil];
}

{
UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"" message:@"二個目" preferredStyle:UIAlertControllerStyleAlert];
[alert addAction:[UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleDefault handler:nil]];
[self presentViewController:alert animated:YES completion:nil];
}


「すでにモーダルで出してるから無理」的な警告が出て「二個目」のアラートは表示されません。


ログ

Warning: Attempt to present <UIAlertController: 0x17e6dca0>  on <ViewController: 0x17d80740> which is already presenting <UIAlertController: 0x17d8dc80>


先のWarning関数のように親ビューコンをその都度求めれば大丈夫のようです。


アラートをHUDとして使っている場合

こちらも最近は(インジケーターがaddSubview出来なくなったので(?))やっている人はマレかもしれませんが、アラートを待ちダイアログ(HUD)として代用しているような場合です。

例ではアラートを閉じて別のモーダルビューを表示させています。


UIAlertViewを使った例

    // HUDもどき

UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"" message:@"アラート" delegate:nil cancelButtonTitle:nil otherButtonTitles:nil];
[alert show];

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

// 別スレッドで何か処理
[NSThread sleepForTimeInterval:0.5];

dispatch_async(dispatch_get_main_queue(), ^{
// アラートを閉じる
[alert dismissWithClickedButtonIndex:0 animated:YES];

// 次のモーダルビューを表示
[self presentViewController:[ModalViewController new] animated:YES completion:nil];
});
});


これをUIAlertControllerに単純に置き換えると以下のようになるでしょうか。


UIAlertControllerを使った例

    // HUDもどき

UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"" message:@"アラート" preferredStyle:UIAlertControllerStyleAlert];
[self presentViewController:alert animated:YES completion:nil];

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

// 別スレッドで何か処理
[NSThread sleepForTimeInterval:0.5];

dispatch_async(dispatch_get_main_queue(), ^{
// アラートを閉じる
[alert dismissViewControllerAnimated:YES completion:nil];

// 次のモーダルビューを表示
[self presentViewController:[ModalViewController new] animated:YES completion:nil];
});
});


しかしこれを実行すると以下のような警告が出て次のモーダルビューが出ません。


ログ

Warning: Attempt to present <ModalViewController: 0x14d76070> on <ViewController: 0x14d16840> while a presentation is in progress!


ただ、アラートを長く表示している場合(例えば0.5秒から2秒へ増やすと)、成功するというのがちょっと気持ち悪いです(潜在的なバグになるかも?)。

以下のように、アラートのdismissのcompletion内で次のモーダルビューを表示するようにすれば表示時間に関わらず成功するようです。


UIAlertControllerを使った例

    UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"" message:@"アラート" preferredStyle:UIAlertControllerStyleAlert];

[self presentViewController:alert animated:YES completion:nil];

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

[NSThread sleepForTimeInterval:0.1];

dispatch_async(dispatch_get_main_queue(), ^{
// アラートを閉じる
[alert dismissViewControllerAnimated:YES completion:^{
// 次のモーダルビューを表示
[self presentViewController:[ModalViewController new] animated:YES completion:nil];
}];
});
});


UIAlertControllerを外部からdismissさせる場合は続きの処理を常にcompletion内で書いた方が安全かもしれません。


おまけ(nil or @"")

UIAlertViewでもUIAlertControllerでも(SDK7でも)タイトル引数にnilを渡すとiOS8ではメッセージ引数で渡した文字列がタイトル文字のように表示されてしまうようです。

nilではなく空文字(@"")を渡せば回避できるようです。


終わりに

嘘が有ったらごめんなさい。

引き続き置き換え作業を頑張ります。