.NET MAUIでiOS向けにモーダル画面をPushModalAsync()で表示し、閉じた直後にDisplayAlert()で結果を通知したいのに、なぜか何も出ない…。例外もなく処理が終わるため原因が掴みにくい現象です。本記事では再現パターン、iOS特有の理由、安定して表示させる設計・実装例をまとめます。
症状:モーダルを閉じた直後に DisplayAlert が「出ないのに成功したように見える」
問題のポイントは「モーダルが閉じたことは検知できているのに、直後のアラートだけが表示されない」点です。典型的には次のような状況になります。
| 観測される現象 | 起きていること(見た目) | やっかいな点 |
|---|---|---|
| モーダルは閉じるまで待てている | TaskCompletionSourceが完了して後続処理に進む | 待機ロジック自体は正しく見える |
DisplayAlert()が表示されない | 例外も出ず、何も起きない | デバッグ時に「成功した」ように見える |
| モーダル内のボタンクリック直後なら表示される | モーダルがまだ表示中のタイミングだと出る | 「コードは正しいのでは?」と誤認しやすい |
Task.Delay(2000)など長めに待つと表示される | 時間を置くとアラートが出る | 根本原因が「時間」なのか分かりづらい |
結論から言うと、iOSでは「モーダルを閉じるUI遷移」と「アラート表示」が同じ系統の画面遷移を使うため、近接したタイミングで連続すると競合してアラートが表示されない(または表示タイミングを逃す)ことがあります。
よくある再現コード:Disappearing で待ってから DisplayAlert
質問内容に近い構造を、最小限の例で整理します。呼び出し元ページでモーダルを開き、Disappearingで閉じたことを検知し、戻ってきたらDisplayAlert()を出す流れです。
public partial class ParentPage : ContentPage
{
public ParentPage()
{
InitializeComponent();
}
private async void OnOpenModalClicked(object sender, EventArgs e)
{
var modal = new MyModalPage();
var tcs = new TaskCompletionSource();
// モーダルが消えたら完了、とする(ここが落とし穴になりやすい)
modal.Disappearing += (_, __) =>
{
tcs.TrySetResult();
};
await Navigation.PushModalAsync(modal);
// ここまでは「閉じるまで待てている」ように見える
await tcs.Task;
// iOSで「出ないことがある」代表例
await DisplayAlert("結果", "モーダルが閉じました", "OK");
}
}
この実装は一見正しく、実際にAndroid等では問題が表面化しないこともあります。しかしiOSでは、Disappearingが発火した「直後」が必ずしも「安全に次の画面遷移(アラート表示)を開始できる瞬間」と一致しない場合があります。
原因の考え方:iOSでは「閉じる」と「アラート表示」が同じ土俵で競合する
この現象を理解すると、対策の方向性が一気にクリアになります。
iOSの画面表示は ViewController の “present/dismiss” が中心
.NET MAUIのモーダル表示も、内部的にはiOS側で画面(ViewController)を提示(present)したり閉じたり(dismiss)したりする流れになります。そしてDisplayAlert()も同様に、iOSではUIAlertControllerを「今見えている画面の上に提示」するUI操作です。
つまり、
- モーダルを閉じる(dismiss)
- アラートを出す(present)
が、どちらも「同じプレゼンテーション(提示)系の遷移」に乗っています。ここが競合ポイントです。
Disappearing は「見えなくなり始めた合図」であって「遷移完了」ではないことがある
Disappearingは便利ですが、iOSのアニメーションや遷移状況によっては「完全に閉じきってUIが落ち着いた状態」より前に発火することがあります。すると、直後にアラートを提示しようとしても、
- 提示元が切り替わり途中
- 提示先(親ページ)がまだ前面に復帰しきっていない
- 別の遷移中として扱われ、提示要求が無視される
といった状態になり、結果として「例外は出ないのにアラートが出ない」挙動につながります。
「長めの Delay で直る」のは、遷移が落ち着くのを待てているから
Task.Delay(2000)のような待機が効くのは、2秒後にはほぼ確実にモーダルのdismissアニメーションや内部状態の切り替えが終わっており、アラートを提示できる状態になっているためです。逆に言うと、100ms程度の短い待機では端末性能や状況によって不足することがあり、再現率が不安定になります。
| タイミング | 意味合い | アラート表示の安定度 | コメント |
|---|---|---|---|
| モーダルのボタン押下直後 | モーダルが前面 | 比較的安定 | 提示元が明確(モーダル)で競合しづらい |
Disappearing直後 | 遷移途中の可能性 | 不安定 | dismissが完全に終わっていないことがある |
親ページのOnAppearing(復帰後) | 親が前面に戻った合図 | 安定 | 「提示元が親ページ」で確定しやすい |
さらにTask.Yield()や短い待機後 | UIループを1回進める | より安定 | 遷移直後の微妙な競合を避けやすい |
対処の基本方針:モーダル閉じ直後ではなく「安全に提示できる瞬間」に寄せる
対処は一言でまとめると、「モーダルが消えたように見える瞬間」と「次のUI(アラート)を安全に出せる瞬間」がズレることがあるので、そのズレを吸収するです。
具体的な選択肢は次の3系統に整理できます。
- 親ページ側の復帰タイミング(
OnAppearing)でアラートを出す - モーダル側から「閉じ完了後」に通知する設計にする
- UIスレッドに「後で実行」として積み、必要なら短い待機を挟む
ここからは、現場で使いやすく、再発しにくい形で実装例を紹介します。
解決策:親ページの OnAppearing でアラートを出す(最も安定しやすい)
最もおすすめなのは、アラート表示を「親ページが前面に戻った後」に寄せる方法です。モーダルが閉じた後、最終的にユーザーが見るのは親ページなので、親ページでアラートを出すのが自然です。
ポイントは、モーダル終了後に「表示したいアラート内容」を親ページに一旦保持し、OnAppearingで消費することです。
public partial class ParentPage : ContentPage
{
private bool _shouldShowAlert;
private string? _alertMessage;
public ParentPage()
{
InitializeComponent();
}
protected override async void OnAppearing()
{
base.OnAppearing();
// 戻ってきたタイミングで1回だけ出す
if (_shouldShowAlert)
{
_shouldShowAlert = false;
var message = _alertMessage ?? "完了しました";
_alertMessage = null;
// 遷移直後の競合を避けるため、UIループを一回進める
await Task.Yield();
await DisplayAlert("結果", message, "OK");
}
}
private async void OnOpenModalClicked(object sender, EventArgs e)
{
var modal = new MyModalPage();
// モーダルから結果を受け取る(後述の「閉じ完了後通知」パターンでもOK)
var tcs = new TaskCompletionSource<string>();
modal.Disappearing += (_, __) =>
{
// ここでは「戻ったら表示する情報をセット」だけにする
tcs.TrySetResult("モーダルが閉じました");
};
await Navigation.PushModalAsync(modal);
var resultMessage = await tcs.Task;
_alertMessage = resultMessage;
_shouldShowAlert = true;
// ここでは DisplayAlert を呼ばない
// → 親ページが前面に戻ったOnAppearingで出す
}
}
このやり方が強い理由は、DisplayAlert()の提示元が「親ページで確定」し、iOSの遷移競合に巻き込まれにくいからです。
“二重表示”を防ぐ小技
OnAppearingは画面遷移やタブ切り替えでも呼ばれるため、フラグを必ず落とす(_shouldShowAlert = false;)のが重要です。また、アラート表示中に再度OnAppearingが呼ばれるケースを避けたい場合は、表示中フラグを追加しても良いです。
| 対策 | 目的 | 実装例 |
|---|---|---|
| フラグを即座に落とす | 同じメッセージが何度も出るのを防ぐ | _shouldShowAlert = false; |
| メッセージを消す | 次回に持ち越さない | _alertMessage = null; |
Task.Yield()を挟む | 遷移直後の競合回避 | await Task.Yield(); |
解決策:モーダル側から「閉じ完了後」を通知する設計にする
Disappearingに頼るのではなく、モーダル側が「自分で閉じた処理が完了した」ことを明示的に通知する設計にすると、挙動が安定しやすくなります。
実装方法はいくつかありますが、実務で扱いやすいのは「モーダルページ自身がTaskを持ち、結果を返す」パターンです。
モーダルページが結果タスクを持つ(TaskCompletionSourceの責務をモーダルに寄せる)
public partial class MyModalPage : ContentPage
{
private readonly TaskCompletionSource<string> _resultTcs = new();
public Task<string> ResultTask => _resultTcs.Task;
public MyModalPage()
{
InitializeComponent();
}
private async void OnOkClicked(object sender, EventArgs e)
{
// 先に閉じる(iOSではこのdismissとalertの競合が起点になりやすい)
await Navigation.PopModalAsync();
// 閉じ処理が進む「次のタイミング」で結果を完了させる
// 親側は ResultTask を await しているので、ここで復帰する
await Task.Yield();
_resultTcs.TrySetResult("OKが押されました");
}
private async void OnCancelClicked(object sender, EventArgs e)
{
await Navigation.PopModalAsync();
await Task.Yield();
_resultTcs.TrySetResult("キャンセルされました");
}
protected override void OnDisappearing()
{
base.OnDisappearing();
// 何らかの理由で閉じられたのに結果が未確定なら、無限待ちを防ぐ
if (!_resultTcs.Task.IsCompleted)
{
_resultTcs.TrySetResult("閉じられました");
}
}
}
親ページ側はこうなります。
private async void OnOpenModalClicked(object sender, EventArgs e)
{
var modal = new MyModalPage();
await Navigation.PushModalAsync(modal);
// ここで「閉じ完了後」に返ってくるように設計できる
var result = await modal.ResultTask;
// iOSで安定させたいなら、ここで即DisplayAlertせず、
// OnAppearingで出す or UIに後積みする(次の解決策)
_alertMessage = result;
_shouldShowAlert = true;
}
この設計にすると、
- 「閉じたことを検知する」のではなく「閉じた後に結果を返す」
- 親側が
Disappearingのタイミングに引きずられない - 無限待ち・イベント解除漏れ・リークのリスクが減る
といったメリットがあります。
解決策:UIスレッドに“後で実行”として積む(Dispatcher / MainThread)
今の構造を大きく変えず、「アラートを出す処理だけを遅延させる」なら、UIスレッドに後で実行する形で積むのが手軽です。
重要なのは次の2点です。
- DisplayAlertはUIスレッドで実行する
- 遷移直後の“同一ターン”を避ける(
Task.Yield()や短いDelayで「次のターン」に送る)
using Microsoft.Maui.ApplicationModel;
private async Task ShowAlertSafelyAsync(string title, string message)
{
await MainThread.InvokeOnMainThreadAsync(async () =>
{
// 直後にUI遷移を重ねないためのワンクッション
await Task.Yield();
// 端末や状況によってはさらに短い待機が効く場合もある
// await Task.Delay(100);
await DisplayAlert(title, message, "OK");
});
}
呼び出し側では、モーダル待機後にこれを使います。
await tcs.Task;
// ここで直接 DisplayAlert せず、後で実行に寄せる
await ShowAlertSafelyAsync("結果", "モーダルが閉じました");
「短い待機」をどの程度入れるかは悩みどころですが、安定性優先なら
Task.Yield()だけで十分なケースTask.Delay(50〜200)程度で収まるケース- 質問のように長めに待つと確実に直るケース
があります。固定の長いDelayはUXを損ねるため、基本は「後で実行 + 必要なら短いDelay」に寄せ、どうしても安定しない場合にのみ段階的に伸ばすのが実務的です。
Delayで無理やり直す前に押さえたい注意点
同じ「アラートが出ない」でも、原因が複数重なっていることがあります。iOSの遷移競合だけでなく、次の点も一緒に確認すると切り分けが速くなります。
提示元ページが“前面にいない”とアラートが握りつぶされることがある
モーダルを閉じた直後は、親ページがまだ前面扱いになっていない可能性があります。さらに、Shell構成や複数ページ構成だと「どのPageでDisplayAlertを呼ぶべきか」がズレやすいです。
| 構成 | アラートを出す候補 | おすすめ |
|---|---|---|
| NavigationPage中心 | 親ページ(戻り先のページ) | 親ページのOnAppearingで表示 |
| Shell利用 | Shell.Current / 現在表示中のPage | 「戻り先の画面」で表示(必要ならShell側に寄せる) |
| 複数Window(iPad等) | Application.Current配下の現在Window | 「今ユーザーが見ているWindow」を起点に表示 |
「どのPageで呼ぶか」を間違えると、タイミング調整以前に表示されません。迷ったら、戻り先のページ(ユーザーが最終的に操作する画面)で出すのが基本です。
イベント購読の解除漏れ・多重購読で挙動が不安定になる
Disappearingを使う場合、ページの再利用や何度もモーダルを開くケースでイベントが多重登録されると、意図しないタイミングでTCSが完了したり、逆に競合しやすくなります。都度作るなら問題になりにくいですが、再利用する設計なら解除もセットで考えてください。
EventHandler? handler = null;
handler = (_, __) =>
{
modal.Disappearing -= handler; // 解除
tcs.TrySetResult();
};
modal.Disappearing += handler;
実務でのおすすめ構成:結果は“戻り先で消費”し、UI表示は“復帰後に1回だけ”
「結果を待つ」と「ユーザーに見せるUI」を分離すると、設計が安定します。
- モーダル:入力・選択・決定(結果を作る)
- 親ページ:結果を受け取る(状態を更新する)
- 親ページの復帰後:必要ならアラート(UI提示)
この分離にすると、アラートに限らず
- トースト表示
- ローディング非表示
- データ再読み込み
など、遷移と相性が悪い処理の置き場所を整理できます。
選び方の早見表:どの対処が向いているか
| 対処法 | 安定性 | 実装コスト | 向いているケース | 注意点 |
|---|---|---|---|---|
親ページのOnAppearingで表示 | 高い | 低〜中 | 戻り先が明確、1回だけ通知したい | OnAppearing多発に備えてフラグ管理が必要 |
| モーダルが結果Taskを返す | 高い | 中 | モーダルを部品化したい、結果を明確に返したい | 予期せぬ閉じ方へのフォールバックがあると安心 |
MainThread/Dispatcher + Task.Yield | 中〜高 | 低 | 構造を変えずに直したい | 環境次第で短いDelay追加が必要になることがある |
長めの固定Delay | 高い(見かけ上) | 最小 | 緊急回避、再現率が高い不具合を止血したい | UX低下・端末差・将来的な調整コスト |
トラブルシュート:それでも出ないときのチェックリスト
最後に、現場で詰まりやすいポイントをチェックリスト化しておきます。
| チェック項目 | 確認ポイント | 対処の方向性 |
|---|---|---|
| アラートはUIスレッドで呼んでいるか | MainThread.InvokeOnMainThreadAsync等で明示 | UIスレッドへ寄せる |
| 提示元ページは「今見えているページ」か | 戻り先のページ/Shellで呼べているか | 親ページのOnAppearingで出す |
Disappearingに依存していないか | 閉じ遷移の途中で復帰していないか | 「復帰後に表示」へ設計変更 |
| イベント多重購読がないか | 開閉を繰り返した時に回数が増えないか | 解除する/ページを使い捨てにする |
| 短い待機で改善するか | Task.Yield()→Task.Delay(100)程度 | 遷移競合が原因の可能性が高い |
まとめ:iOSでは「閉じた直後=次のUIを出せる瞬間」ではないことがある
この問題は、コードが間違っているというより、iOSのUI遷移の性質(モーダルdismissとアラートpresentが近接すると競合する)に起因して起こります。対策としては、
- アラートは“戻り先(親ページ)の復帰後”に出す(
OnAppearingで1回だけ) - モーダル側から“閉じ完了後”に結果を返す設計に寄せる
- MainThread/Dispatcher +
Task.Yield()で「次のターン」に送る
のいずれかが堅実です。特に「親ページのOnAppearingで表示」は、アラート以外のUI更新にも応用でき、iOS特有のタイミング問題に強い実装になります。

コメント