日程Fit|「いつ空いてますか?」の往復はもう不要。候補日を選んでURLを送るだけ|登録不要|今すぐ無料で使う →

.NET MAUI iOSでモーダル閉じ直後にDisplayAlertが表示されない原因と対処法(PushModalAsync/TaskCompletionSource)

.NET MAUIでiOS向けにモーダル画面をPushModalAsync()で表示し、閉じた直後にDisplayAlert()で結果を通知したいのに、なぜか何も出ない…。例外もなく処理が終わるため原因が掴みにくい現象です。本記事では再現パターン、iOS特有の理由、安定して表示させる設計・実装例をまとめます。

日程Fit。無料・登録不要。「いつ空いてる?」を、ひとつのリンクで。リンクを送って、○△×でかんたん日程調整。無料で日程を作る。
目次

症状:モーダルを閉じた直後に 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特有のタイミング問題に強い実装になります。

この記事を書いた人

実務の現場で詰まりがちなポイントを地図にするITブログ「IT trip」を運営。Windows/Office(Teams・Excel)からSQL、サーバ運用、ガジェットまで、再現性のある手順と“なぜそうなるか”を丁寧に解説します。読んだらすぐ試せること、そして迷った人の次の一歩が見えることを大切にしています。

コメント

コメントする

目次