.NET MAUIでNavigation.PushAsyncした直後に重い処理を走らせると、画面が固まったりActivityIndicatorが見えないことがあります。OnAppearingよりも“ページ表示後”に寄せたいとき、使えるイベントと限界、現場で破綇しにくい実装パターンを整理します。
.NET MAUIで「ページが表示された直後」に確実に走る標準イベントはあるのか
先に結論から言うと、現時点の.NET MAUIには「ユーザーに見えるように描画が完了した直後」をクロスプラットフォームで保証してくれる、分かりやすいページライフサイクルイベントは用意されていません。質問でよく期待される「レンダリング完了」「表示完了」のような合図は、標準APIとしてはまだ弱い(もしくは意図的に用意されていない)という理解が現実的です。
特に混乱しやすいのが OnAppearing() で、名前の印象に反して「画面に見えてから」ではなく、あくまで“表示に入るタイミング(表示準備が整ったタイミング)”で呼ばれます。ナビゲーションのアニメーション、レイアウト計測、バインディング適用、画像デコードなどが絡むため、OnAppearingの時点でユーザーが視認できる状態になっているとは限りません。
なぜ「表示直後」を保証するのが難しいのか(MAUIの事情)
「表示された直後」は人間には分かりやすい概念ですが、フレームワーク側で統一するのは意外に難しいです。理由はざっくり次の通りです。
- 描画は段階的:ページの生成 → ビジュアルツリーへの追加 → 計測(Measure)→ 配置(Arrange)→ 描画、と段階があり、途中で何度もやり直しが発生します。
- プラットフォーム差:iOS/Android/Windowsでは「表示」の定義やナビゲーションアニメーションの完了タイミングが異なります。
- ナビゲーションはアニメーションを伴う:画面が“完全に見える”のはアニメーション終了後ですが、イベントはその前に発火することがあります(特にiOS/Mac Catalystで顕著)。
- 重い処理がUIスレッドを塞ぐ:OnAppearingなどで同期処理・重い処理をすると、UI更新自体が遅れ、「スピナーが出る前に処理が走る」「画面が固まる」が起きます。
つまり「表示直後」を狙うには、目的に応じて “十分に遅いタイミング” を自前で作る、という発想が必要になります。
まず押さえる:よく使うイベントと「何が保証されるか」
| タイミング/イベント | 発火の目安 | 保証できること | 向いている用途 | 注意点 |
|---|---|---|---|---|
| コンストラクタ | ページ生成時 | オブジェクトが存在する | DI注入・イベント購読・初期値設定 | UIはまだ“表示”と無関係 |
| OnAppearing / Appearing | 表示に入る直前〜直後(保証なし) | ページが表示対象になった | 毎回の表示更新、軽い処理 | “描画完了後”ではない |
| Loaded(VisualElement.Loaded) | 要素が構築され、プラットフォームのビジュアルツリーに追加された時 | ツリーに載った | 初回の初期化、初回だけ走らせる処理 | 計測前に発火することがある(サイズ保証なし) |
| SizeChanged | サイズが変わった時 | 少なくとも一度は計測・配置が走った可能性が高い | 幅/高さが必要な処理、スクロール位置調整 | 何度も発火する(回転・リサイズ等) |
| NavigatedTo / OnNavigatedTo | ページにナビゲートされた直後 | 「ナビゲーション完了相当」を通知 | ナビゲーションを起点にする処理 | iOS/Mac Catalystではアニメーション完了前に発火し得る |
表の通り、Loadedは“ツリーに載った”を検知できる一方で、サイズ確定や描画完了は保証しません。そしてNavigatedToも「ユーザーに見えた瞬間」と一致しないケースがあり得ます。だからこそ、用途別の組み合わせが重要です。
代替案の本命:VisualElement.Loadedで「ロード完了寄り」を取る
.NET MAUIには VisualElement.Loaded イベントがあり、要素が構築されてプラットフォームのビジュアルツリーに追加されたときに発火します。ContentPageもVisualElement系なので、ページ自身(またはルート要素)で購読できます。
最小構成:ページでLoadedを1回だけ受ける
public partial class DetailPage : ContentPage
{
private bool _initialized;
public DetailPage()
{
InitializeComponent();
// 初回だけ走らせたいならLoadedで購読→解除が鉄板
Loaded += OnLoaded;
}
private void OnLoaded(object? sender, EventArgs e)
{
if (_initialized) return;
_initialized = true;
Loaded -= OnLoaded;
// ここで「ロード後にやりたい処理」
StartLazyLoad();
}
private void StartLazyLoad()
{
// 例:軽い初期化、一覧のScrollToなど
}
}
ポイントは「Loadedは初回だけでいいのか、表示のたびに必要なのか」を最初に決めることです。初回だけなら解除しておくと、ページ再利用やテンプレート再適用のようなケースでも意図せず複数回走る事故が減ります。
注意:Loadedは「サイズ確定」ではない
Microsoft LearnのLoadedの備考にもある通り、Loadedは計測(Measure)より前に発火し得るため、Width/Heightが欲しい処理には不十分なことがあります。
例えば次のようなケースはLoaded単体では事故りやすいです。
- 初回表示で「特定位置へスクロール」するが、まだレイアウトが固まっていない
- 要素のWidth/Heightを使って計算したい(Canvas、画像のトリミング、座標計算など)
- ActivityIndicatorを「見せてから」重い処理をしたいが、スピナー表示が間に合わない
レイアウト確定が必要なら:Loaded+SizeChangedで“1回目のサイズ確定”を拾う
「ツリーに載った」だけでなく、幅・高さが確定した後に処理したい場合は、Loadedを起点にしてSizeChangedを1回だけ受けるのが実務で安定します。
public partial class ChartPage : ContentPage
{
private bool _layoutHandled;
public ChartPage()
{
InitializeComponent();
Loaded += OnPageLoaded;
}
private void OnPageLoaded(object? sender, EventArgs e)
{
// Loadedは初回だけでOKなら解除
Loaded -= OnPageLoaded;
// サイズ確定を待つ
SizeChanged += OnSizeChangedOnce;
}
private void OnSizeChangedOnce(object? sender, EventArgs e)
{
// サイズが未確定っぽいときは早期return(環境差対策)
if (Width <= 0 || Height <= 0) return;
if (_layoutHandled) return;
_layoutHandled = true;
SizeChanged -= OnSizeChangedOnce;
// ここでサイズ依存の処理
InitializeChartLayout(width: Width, height: Height);
}
private void InitializeChartLayout(double width, double height)
{
// 例:サイズに応じた描画、ScrollTo、画像の計算など
}
}
SizeChangedは「サイズが変わるたび」なので、回転・マルチウィンドウ・デスクトップでのリサイズなどでも再び発火します。初回だけにしたいなら、上のようにフラグ+解除で制御します。
「表示されてから」を体感的に改善する実務ワークアラウンド
イベントだけで完全な描画完了を掴めない以上、ユーザー体験を崩さないための“やり方”が重要になります。ここでは、タイマー頼みの力技を避けつつ、現場で効くパターンをまとめます。
| やりたいこと | 推奨パターン | 狙い | 実装のコツ |
|---|---|---|---|
| 初回表示後にデータを読みたい | Loadedで開始(軽い処理) | ページ生成直後の詰まりを回避 | Loadedは1回だけで解除 |
| 幅/高さが必要な初期化をしたい | Loaded→SizeChanged(1回) | レイアウト確定後に寄せる | Width/Height>0を確認して解除 |
| スピナーを確実に見せてから重い処理 | 表示→UIに“1回譲る”→処理開始 | まず描画を通す | Task.YieldやDispatcherで次のタイミングへ |
| ページ再表示ごとに更新したい | OnAppearing / NavigatedToで更新 | 毎回の更新を担保 | 重い処理は非同期+キャンセル |
「UIに1回譲る」:Task.Yieldで体感を改善する
「タイマーで数十ms待つ」の代わりに、まずはUIスレッドに制御を返して描画チャンスを作る、という発想が有効です。具体的には、OnAppearingから非同期メソッドを呼び、最初に await Task.Yield() を挟みます。
public partial class StartPage : ContentPage
{
private CancellationTokenSource? _cts;
protected override void OnAppearing()
{
base.OnAppearing();
// 表示のたびに走る処理(重いなら非同期+キャンセル)
_cts?.Cancel();
_cts = new CancellationTokenSource();
_ = RunAfterAppearingAsync(_cts.Token);
}
protected override void OnDisappearing()
{
_cts?.Cancel();
base.OnDisappearing();
}
private async Task RunAfterAppearingAsync(CancellationToken token)
{
// まずUIに描画チャンスを与える
await Task.Yield();
// ここでスピナーを見せたいなら、Visible切り替え→Yield→処理開始が効く
LoadingIndicator.IsVisible = true;
LoadingIndicator.IsRunning = true;
await Task.Yield();
try
{
await LoadDataAsync(token);
}
catch (OperationCanceledException)
{
// 画面遷移などでキャンセルされた場合
}
finally
{
LoadingIndicator.IsRunning = false;
LoadingIndicator.IsVisible = false;
}
}
private async Task LoadDataAsync(CancellationToken token)
{
// 例:API呼び出し、DB読み込み、画像準備など
await Task.Delay(1, token); // サンプル(実際はI/O処理)
}
}
Task.Yieldは「一定時間待つ」ではなく、「今の処理を一旦終えて次のメッセージループへ回す」イメージなので、タイマーよりも意図が明確で副作用が少ないことが多いです。それでもプラットフォームや負荷状況で完全には保証できない点は理解しておきましょう。
どうしても必要なら短いDelayを使う(最終手段)
LoadedやSizeChanged、Task.Yieldを組み合わせても「スピナーが見えない」「初回だけ固まる」が残ることがあります。これは、画像デコードや初回バインディングのコストがUIスレッドを占有し、描画の機会がさらに後ろに押されるためです。その場合だけ、50〜100ms程度のごく短いDelayを入れるのは、現場では珍しくありません(ただし依存しすぎないことが重要です)。
NavigatedTo / OnNavigatedToは使える?(ただし万能ではない)
.NET MAUIのPageには NavigatedTo イベントや OnNavigatedTo が用意されています。ナビゲーションを起点に処理を整理したい場合には有用です。
public partial class ListPage : ContentPage
{
protected override void OnNavigatedTo(NavigatedToEventArgs args)
{
base.OnNavigatedTo(args);
// ナビゲーション直後にやりたい処理(例:選択状態の復元など)
RestoreSelection();
}
private void RestoreSelection()
{
// 例:前回の選択を復元
}
}
ただし、NavigationPageのドキュメントにもある通り、iOS/Mac Catalystではナビゲーションイベントがネイティブアニメーションの完了前に発火する場合があるため、「ユーザーに見えてから」を厳密に担保する用途には向きません。
Shellアプリのライフサイクルも「表示完了」を保証するわけではない
Shellを使っている場合、ナビゲーション時にどのオブジェクトがAppearing/Disappearingを出すか、といった整理が必要になります。Microsoft LearnにもShellナビゲーションに伴うAppearing/Disappearingの動きがまとめられています。
ただし、ここでのAppearingもやはり「描画完了後」を保証するものではありません。Shellであっても、基本戦略は同じで、LoadedやSizeChanged、非同期化などで“十分に遅いタイミング”を作ります。
Loadedにも落とし穴がある:発火の回数・ペアリング問題
Loadedは便利ですが、常に「1回だけ」「必ずUnloadedと対になる」とは限りません。ControlTemplateの再適用、要素の差し替え、プラットフォーム差などで、Loaded/Unloadedの発火が想定とズレることがある、という報告もあります。実装側では次の対策が無難です。
- イベントは基本“購読したら解除”:ページ破棄や再利用時の多重実行・メモリリークを防ぐ。
- 初回だけやりたい処理はフラグでガード:Loadedが複数回起きても破綻しない。
- 「表示のたびに必要」ならOnAppearing/NavigatedToへ寄せる:Loadedに“毎回”を期待しない。
目的別:実装パターンの選び方(迷ったときの指針)
最後に「結局どれを使うべき?」を目的別に整理します。
| 目的 | おすすめ | 理由 |
|---|---|---|
| 初回だけ初期化したい(DI後のセットアップなど) | Loaded(1回で解除) | ツリーに載った後に走らせられ、コンストラクタより安全 |
| 初回表示でスクロール位置を合わせたい | Loaded→SizeChanged(1回) | レイアウト確定後の方がScrollToが安定 |
| ページ表示のたびにデータ更新したい | OnAppearing または NavigatedTo | ページ再表示がトリガーになる |
| とにかく「まずUIを出してから」重い処理をしたい | OnAppearing内で非同期化→Task.Yield | UIに描画チャンスを与え、体感を改善しやすい |
| どうしても“完全に見えた後”が必要 | プラットフォーム固有実装(上級) | ネイティブのViewDidAppear等に寄せるしかない |
まとめ:.NET MAUIでは「表示完了イベント」はない前提で設計する
.NET MAUIで「ページが表示された直後に処理を実行したい」と思ったとき、最初に受け入れるべき現実は、“描画完了を保証する標準イベントは(まだ)用意されていない”という点です。
その上で、
- ツリーに載った後に寄せたい → Loaded
- 幅・高さなどレイアウト確定が必要 → Loaded+SizeChanged
- 体感を良くする(UIを先に見せる) → 非同期化+Task.Yield
という形で、目的に合わせてイベントを組み合わせるのが、現時点での最も実務的で再現性の高い解決策です。

コメント