.NET MAUI の iOS 版では AppDelegate の OnActivated() が、バックグラウンド復帰だけでなく、電話着信や通知センター表示などの割り込み後にも呼ばれます。本記事では「何によって呼ばれたか」を特定できない理由を説明し、実務で使える代替案(推測ロジックと専用ハンドラ設計)を具体的に解説します。
OnActivated() が呼ばれるときに起きていること
OnActivated() は、iOS のアプリライフサイクルで「アプリ(またはシーン)がアクティブになった」という状態変化を通知する入口です。ここで重要なのは、“アクティブになった事実” は分かっても、“なぜアクティブになったのか(直前に何が割り込んだのか)” は別問題という点です。
たとえば、ユーザーが別アプリに切り替えて戻ってきた場合も、電話着信で一時的に非アクティブになって戻った場合も、最終的には「再び操作可能な状態になった」ので OnActivated() が呼ばれます。見た目は同じでも、原因は複数あり得ます。
よくある「OnActivated() が呼ばれる」パターン
| ユーザーの体験 | アプリの状態としては | OnActivated() の呼ばれ方 |
|---|---|---|
| ホームに戻って、再度アプリを開く | バックグラウンド → フォアグラウンド | 復帰時に呼ばれやすい |
| 電話着信・FaceTime・アラーム等の割り込み後に戻る | 一時的に非アクティブ → アクティブ | 割り込み終了後に呼ばれやすい |
| 通知センター/コントロールセンターを出して閉じる | 一時的に非アクティブ → アクティブ | 閉じたタイミングで呼ばれやすい |
| 権限ダイアログ(位置情報/カメラ等)を表示して許可/拒否 | 表示中に非アクティブ扱いになる場合がある | ダイアログ終了後に呼ばれることがある |
結論:OnActivated() が「何によって呼ばれたか(割り込み元)」は特定できない
先に結論を明確にします。一般的な iOS アプリ(.NET MAUI を含む)では、電話着信や他アプリへの切り替えなど「割り込み元が何か/どのアプリか」を特定する公開 API は提供されていません。
iOS はプライバシー/セキュリティの観点から、あるアプリが「他のアプリの存在」や「直前に前面に出ていたアプリ」を推測できるような情報を基本的に渡しません。従って、OnActivated() の中で “原因アプリ名” や “割り込み種別” を確定する設計はできません。
また、非公開(プライベート)API を使って無理やり取得するアプローチは、動作保証がなく、App Store 審査でリジェクトされる可能性が高いため、業務アプリ・公開アプリでは現実的ではありません。
「分かること/分からないこと」を切り分ける
| 知りたいこと | 結論 | 補足 |
|---|---|---|
| どのアプリが割り込んだか | 分からない | OS が公開 API として提供していない |
| 電話着信が原因か | 原則分からない | オーディオ等の「特定領域の割り込み」は検知できても、OnActivated の原因特定には直結しない |
| バックグラウンドから復帰したか | ある程度は推測できる | DidEnterBackground を通ったかのフラグで判定 |
| URL スキーム/ユニバーサルリンク等で起動・復帰したか | 分かる | OpenUrl/ContinueUserActivity などの専用ハンドラで確定できる |
| プッシュ通知タップが起点か | 分かる | 通知関連のデリゲート(またはライブラリ)で確定できる |
現実的な落としどころ:OnActivated() を「原因判定の場」にしない
OnActivated() を原因分岐の中心にしてしまうと、割り込みパターンが増えるたびに例外処理が増え、テストも破綻しがちです。実務では、OnActivated は “復帰後の整備(リフレッシュ)” を行う場所として割り切るのが安全です。
- 画面が操作可能になったので、必要ならデータを再読み込みする
- タイマーや監視を再開する(ただし重い処理はデバウンスする)
- トークン期限やネットワーク状態など、復帰直後に確認すべきものを点検する
一方で、「URL で起動された」「通知をタップされた」など、原因を確定できる経路は OnActivated ではなく専用ハンドラで拾うのが王道です。
対処法:バックグラウンド復帰かどうかだけは自前フラグで推測する
iOS のライフサイクルイベントを使うと、OnActivated() 直前に DidEnterBackground を通っているかどうかで「バックグラウンド復帰っぽい」を判定できます。もちろん電話着信かどうか、どのアプリが原因かは分かりませんが、UI 更新やデータ再同期の粒度調整には十分役立ちます。
イベントの流れをイメージする
| ケース | よくあるイベント順(概念) | 推測できること |
|---|---|---|
| バックグラウンドへ → 復帰 | OnResignActivation → DidEnterBackground → WillEnterForeground → OnActivated | DidEnterBackground を通った=背景復帰 |
| 一時割り込み → 復帰 | OnResignActivation →(背景には行かない)→ OnActivated | 背景復帰ではない=割り込み復帰の可能性 |
| 初回起動 | FinishedLaunching → OnActivated | 起動直後の初回アクティブ化 |
AppDelegate でのシンプル実装例(推測レベル)
以下は「バックグラウンド復帰かどうか」を推測するだけの最小構成です。割り込み原因の特定ではなく、復帰処理の粒度を変える目的で使います。
using Foundation;
using UIKit;
namespace YourApp;
[Register("AppDelegate")]
public class AppDelegate : MauiUIApplicationDelegate
{
bool _wentToBackground;
protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp();
public override void DidEnterBackground(UIApplication application)
{
base.DidEnterBackground(application);
_wentToBackground = true;
}
public override void OnActivated(UIApplication application)
{
base.OnActivated(application);
if (_wentToBackground)
{
// バックグラウンド復帰と推測できる
// 例:サーバー再同期、認証状態チェック、一覧のリロード等
}
else
{
// 割り込み復帰 or 起動直後など(原因は特定不可)
// 例:軽い UI の再描画だけに留める、必要に応じてデバウンスする
}
_wentToBackground = false;
}
}
対処法:割り込み復帰は「ひとまとめ」に扱い、OnResignActivation とセットで考える
「バックグラウンド復帰」ではないケースの多くは、電話着信・通知センター・コントロールセンター・システムダイアログなど、アプリが一時的に非アクティブになる割り込みです。ここで大事なのは、原因を細分化するのではなく、“割り込み復帰” としてまとめて扱うことです。
実装としては、OnResignActivation(非アクティブ化)をフラグ化し、DidEnterBackground と組み合わせて分類します。
推測ロジックの例(3分類)
| OnResignActivation | DidEnterBackground | OnActivated での分類 | 実務上の扱い |
|---|---|---|---|
| いいえ | いいえ | 初回起動直後など | 初期化・初回ロード |
| はい | はい | バックグラウンド復帰 | 必要なら再同期(やや重めOK) |
| はい | いいえ | 割り込み復帰 | 軽めの復帰処理(UI整備中心) |
AppDelegate 実装例(割り込み復帰も推測する)
public class AppDelegate : MauiUIApplicationDelegate
{
bool _resignedActive;
bool _wentToBackground;
protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp();
public override void OnResignActivation(UIApplication application)
{
base.OnResignActivation(application);
_resignedActive = true;
}
public override void DidEnterBackground(UIApplication application)
{
base.DidEnterBackground(application);
_wentToBackground = true;
}
public override void OnActivated(UIApplication application)
{
base.OnActivated(application);
if (_wentToBackground)
{
// バックグラウンド復帰
}
else if (_resignedActive)
{
// 割り込み復帰(電話着信など“何が原因か”はここでは分からない)
}
else
{
// 初回起動直後など
}
_resignedActive = false;
_wentToBackground = false;
}
}
対処法:原因が分かる経路は「OnActivated 以外」で確定する
iOS では、原因を特定できる復帰経路がいくつかあります。代表例は URL スキーム、ユニバーサルリンク、Handoff、通知タップ などです。これらは OnActivated() に頼るのではなく、専用のイベント(デリゲート)で受け取る設計にするとブレません。
| 起動/復帰のきっかけ | iOS 側で拾う代表的な場所 | OnActivated との関係 |
|---|---|---|
| URL スキーム(myapp://…) | OpenUrl | OpenUrl の後に OnActivated が来ることがある |
| ユニバーサルリンク(https://…) | ContinueUserActivity | ContinueUserActivity の後に OnActivated が来ることがある |
| Handoff / Spotlight など | ContinueUserActivity | 同上 |
| プッシュ通知タップ | 通知デリゲート(UNUserNotificationCenter 等) | 通知処理の後に OnActivated が来ることがある |
AppDelegate に専用ハンドラを用意する例
URL で起動されたのか、ユニバーサルリンクなのかは、この時点で確定できます。OnActivated で無理に推測しないのがポイントです。
public override bool OpenUrl(UIApplication app, NSUrl url, NSDictionary options)
{
// ここで URL スキーム起動を確定できる
// 例:url.AbsoluteString を解析して画面遷移
return base.OpenUrl(app, url, options);
}
public override bool ContinueUserActivity(
UIApplication application,
NSUserActivity userActivity,
UIApplicationRestorationHandler completionHandler)
{
// ここでユニバーサルリンク/Handoff 等を確定できる
return base.ContinueUserActivity(application, userActivity, completionHandler);
}
通知タップの経路は通知デリゲートで拾う
プッシュ通知(またはローカル通知)のタップを起点に画面遷移させたい場合も、OnActivated() で推測するのではなく、通知のデリゲートで “通知が押された” 事実を受け取って処理します。ここでペイロードを保持しておき、必要なら MAUI 側のナビゲーションに渡す設計にするとブレません。以下は、後述の IActivationTracker に「通知タップ」を記録する例です。
using System;
using UserNotifications;
public class NotificationDelegate : UNUserNotificationCenterDelegate
{
public override void DidReceiveNotificationResponse(
UNUserNotificationCenter center,
UNNotificationResponse response,
Action completionHandler)
{
ActivationTrackerHolder.Tracker?.SetTrigger(ActivationTrigger.NotificationTap);
// response.Notification.Request.Content.UserInfo などからペイロードを取得して保持する
completionHandler();
}
}
デリゲートの設定は、iOS プロジェクト側(例:FinishedLaunching)で行います。
using Foundation;
using UIKit;
using UserNotifications;
public override bool FinishedLaunching(UIApplication application, NSDictionary launchOptions)
{
UNUserNotificationCenter.Current.Delegate = new NotificationDelegate();
return base.FinishedLaunching(application, launchOptions);
}
さらに精度を上げたいとき:自分で起こした「離脱」はフラグで記録する
「他アプリが割り込んだか」は分かりませんが、自分のアプリが能動的に起こした離脱(例:外部ブラウザを開く、システム設定へ飛ばす、権限リクエストを出す)は、事前にフラグを立てておけば復帰後に判別できます。これは iOS の制限に抵触せず、現場でかなり効きます。
| 「自分で起こした」代表例 | フラグを立てるタイミング | 復帰後の扱い |
|---|---|---|
| 外部ブラウザ(Safari)を開く | Launcher.OpenAsync などの直前 | 戻ったら画面を戻す/軽い更新だけ |
| 権限要求ダイアログを出す | 権限リクエスト API 呼び出し直前 | 許可/拒否の結果に応じて UI を更新 |
| 設定アプリへ遷移(権限の案内) | 設定への導線を押した直前 | 戻ったら権限状態を再チェック |
実装例:復帰理由をアプリ内で共有する(MAUI へ渡す)
AppDelegate の中だけでフラグを持つと、画面や ViewModel から参照しづらく、復帰処理の責務が AppDelegate に集まりがちです。そこで実務では「最後に起きた復帰の種類」をアプリ内サービスとして持ち、必要な場所から参照できるようにすると設計が安定します。
ポイントは、“割り込み原因(電話・他アプリ等)” を当てにいかないこと。代わりに、次の2軸だけを記録します。
- 遷移(Transition):起動/背景復帰/割り込み復帰 といった状態遷移
- トリガー(Trigger):URL スキーム/ユニバーサルリンク/通知タップなど、専用ハンドラで確定できる入口
遷移とトリガーを分けて持つメリット
| やりたいこと | 見るべき値 | 理由 |
|---|---|---|
| 背景復帰だけ重めに再同期したい | Transition | 背景復帰かどうかはイベント順で推測できる |
| URL で特定画面に飛ばしたい | Trigger(+URL本体) | OpenUrl で確定でき、OnActivated に依存しない |
| 通知タップのときだけバッジを消したい | Trigger | 通知デリゲートで確定できる |
モデルとトラッカーの最小実装
using System;
public enum ActivationTransition
{
Unknown,
ColdStart,
ForegroundFromBackground,
ResumeFromInterruption
}
public enum ActivationTrigger
{
None,
OpenUrl,
ContinueUserActivity,
NotificationTap
}
public interface IActivationTracker
{
ActivationTransition LastTransition { get; }
ActivationTrigger LastTrigger { get; }
DateTimeOffset LastChangedAt { get; }
void SetTransition(ActivationTransition transition);
void SetTrigger(ActivationTrigger trigger);
}
public class ActivationTracker : IActivationTracker
{
public ActivationTransition LastTransition { get; private set; } = ActivationTransition.Unknown;
public ActivationTrigger LastTrigger { get; private set; } = ActivationTrigger.None;
public DateTimeOffset LastChangedAt { get; private set; } = DateTimeOffset.MinValue;
public void SetTransition(ActivationTransition transition)
{
LastTransition = transition;
LastChangedAt = DateTimeOffset.UtcNow;
}
public void SetTrigger(ActivationTrigger trigger)
{
LastTrigger = trigger;
LastChangedAt = DateTimeOffset.UtcNow;
}
}
// AppDelegate と MAUI 側で共有するための簡易ホルダー(DI で渡せる構成なら不要)
public static class ActivationTrackerHolder
{
public static IActivationTracker? Tracker { get; set; }
}
MauiProgram で DI 登録し、必要ならホルダーへ渡す
using Microsoft.Extensions.DependencyInjection;
public static class MauiProgram
{
public static MauiApp CreateMauiApp()
{
var builder = MauiApp.CreateBuilder();
builder
.UseMauiApp<App>();
builder.Services.AddSingleton<IActivationTracker, ActivationTracker>();
var app = builder.Build();
// AppDelegate など MAUI 外から参照するために保持(静的にしたくない場合は別設計に)
ActivationTrackerHolder.Tracker = app.Services.GetService<IActivationTracker>();
return app;
}
}
AppDelegate で更新する(原因の確定ではなく分類)
using Foundation;
using UIKit;
public class AppDelegate : MauiUIApplicationDelegate
{
bool _resignedActive;
bool _wentToBackground;
bool _firstActivated = true;
IActivationTracker? Tracker => ActivationTrackerHolder.Tracker;
protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp();
public override void OnResignActivation(UIApplication application)
{
base.OnResignActivation(application);
_resignedActive = true;
}
public override void DidEnterBackground(UIApplication application)
{
base.DidEnterBackground(application);
_wentToBackground = true;
}
public override void OnActivated(UIApplication application)
{
base.OnActivated(application);
if (_firstActivated)
{
Tracker?.SetTransition(ActivationTransition.ColdStart);
_firstActivated = false;
}
else if (_wentToBackground)
{
Tracker?.SetTransition(ActivationTransition.ForegroundFromBackground);
}
else if (_resignedActive)
{
Tracker?.SetTransition(ActivationTransition.ResumeFromInterruption);
}
else
{
Tracker?.SetTransition(ActivationTransition.Unknown);
}
_resignedActive = false;
_wentToBackground = false;
}
public override bool OpenUrl(UIApplication app, NSUrl url, NSDictionary options)
{
Tracker?.SetTrigger(ActivationTrigger.OpenUrl);
return base.OpenUrl(app, url, options);
}
public override bool ContinueUserActivity(
UIApplication application,
NSUserActivity userActivity,
UIApplicationRestorationHandler completionHandler)
{
Tracker?.SetTrigger(ActivationTrigger.ContinueUserActivity);
return base.ContinueUserActivity(application, userActivity, completionHandler);
}
}
MAUI 側での使い方イメージ
ページ表示時(OnAppearing 相当)にトラッカーを見て、背景復帰だけ再同期する、といった書き方ができます。
public class SampleViewModel
{
readonly IActivationTracker _tracker;
public SampleViewModel(IActivationTracker tracker)
{
_tracker = tracker;
}
public async Task OnAppearingAsync()
{
if (_tracker.LastTransition == ActivationTransition.ForegroundFromBackground)
{
await ReloadAsync();
}
}
Task ReloadAsync() => Task.CompletedTask;
}
この構成なら、「OnActivated が何で呼ばれたか」を当てにいかずに、必要な復帰処理を安定して実装できます。
運用で効く:ログを残して “原因ではなく状況” を把握する
OnActivated の原因を OS からもらえない以上、最終的に頼りになるのはアプリ内ログです。特に、実機でしか再現しない割り込み(電話・アラーム・通知など)は、ログがないと切り分け不能になります。
最低限ログに入れておきたい項目
| 項目 | 例 | 目的 |
|---|---|---|
| イベント名 | OnResignActivation / DidEnterBackground / OnActivated | 発生順の確認 |
| タイムスタンプ | UTC とローカル両方でも可 | 頻度・直前の操作との相関 |
| 画面・ルート | LoginPage / DetailPage など | どの画面で起きたか |
| 推測した復帰種別 | ForegroundFromBackground / ResumeFromInterruption | 復帰処理の分岐が妥当か検証 |
| ネットワーク状態 | Online / Offline | 復帰後の通信失敗の原因特定 |
よくある落とし穴と設計のコツ
OnActivated で毎回“重い再同期”を走らせない
電話着信や通知センター操作のたびに OnActivated() が呼ばれると、短時間に何度も復帰処理が走ります。ここで毎回フル同期や重い API 呼び出しをすると、バッテリー・通信量・体感速度が悪化します。背景復帰のみ重めに、割り込み復帰は軽めに、といった粒度調整が重要です。
「原因特定」ではなく「復帰後に整えておくべきこと」を決める
- 入力途中のフォームがあるなら、キーボード状態やフォーカスを整える
- セッションが切れやすいなら、トークン期限と再認証導線を整える
- リアルタイム要素があるなら、購読(SignalR 等)を再接続する
- 外部へ出た可能性があるなら、権限・設定値を再確認する
それでも “電話っぽい” を検知したいときは目的を絞る
どうしても「電話着信中に音が止まった」「通話で録音が中断された」など、特定領域の割り込みを扱いたい場合は、ライフサイクルではなく該当領域の通知(例:オーディオセッションの割り込み通知)で設計します。ただし、この場合でもどのアプリが原因かを特定することはできません。あくまで「自アプリが扱う機能が中断された」という事実に寄せるのが安全です。
まとめ
OnActivated()は「アクティブになった」ことを示すだけで、割り込み元(どのアプリ/要因か)は特定できない- 代替案として、
DidEnterBackgroundを通ったかどうかで「背景復帰」を推測し、割り込み復帰はまとめて扱う - URL スキーム/ユニバーサルリンク/通知タップなど、原因が確定できる経路は専用ハンドラで処理する
- 自分で起こした離脱(外部ブラウザ、設定遷移、権限要求など)はフラグで記録し、復帰後の UX を最適化する
この考え方に切り替えると、「原因を当てる」ための不安定な分岐が減り、OnActivated のたびにアプリが暴れる問題も抑えられます。結果として、.NET MAUI iOS アプリのライフサイクル実装がシンプルで堅牢になります。

コメント