Xamarin から .NET 8 の .NET MAUI へ移行した後、Azure Notification Hubs から送信すると「成功」なのに端末に通知が出ない…という相談は珍しくありません。Android は受信実装と通知権限、iOS は APNs の aps ペイロードと署名設定が落とし穴です。
現象を正しく捉える:「Hub で成功」=「端末に届いた」ではない
Azure Notification Hubs の送信結果が「成功」と表示されても、それはHub が送信要求を受け付け、各プッシュ基盤(FCM / APNs)へ転送できた可能性が高いという意味で、端末側で通知が表示されたことを保証するものではありません。特に Xamarin → .NET MAUI の移行では、受信側の構成(サービス登録・権限・エンタイトルメント)と、送信側の構成(Hub の資格情報・トークン登録・ペイロード)が噛み合わず、表面上は「成功」なのに何も起きない状態になりがちです。
| よくある症状 | 起きやすい原因 | 最初に確認するポイント |
|---|---|---|
| Hub から送ると成功、端末は無反応(Android) | 受信サービス未登録、通知権限未許可、トークン不一致、チャンネル未設定、バックグラウンド制限 | logcat で受信処理に入っているか/Android 13+ の権限/トークンのログ出力 |
| 同じトークンに Firebase コンソールから送ると届く | 端末・Firebase は正常。Hub 側の資格情報や登録、または Hub に投げるペイロードが合っていない | Hub の FCM v1 設定・プロジェクト一致/登録しているトークンが最新か |
| Android は改善したが iOS だけ届かない/「ペイロードが無効」 | APNs 形式(aps)が無い、環境(development/production)不一致、エンタイトルメント不足 | aps を含む APNs ペイロード/aps-environment/Hub の APNs 資格情報 |
以降では、実務で遠回りしないために「切り分け → Android → Hub → iOS」の順で潰していく手順を、具体的なチェック項目とサンプルを交えて整理します。
切り分けで9割決まる:最小構成・デバッグ以外・ログの三点セット
プッシュ通知は「端末側」「クラウド側」「ネットワーク」「OS 制約」が絡むため、いきなり本番アプリで追うほど迷宮入りします。まずは原因を一点に絞れる状態を作ります。
最小構成の .NET MAUI プロジェクトで再現性を確認する
- .NET 8 の新規 .NET MAUI アプリを作成し、プッシュ通知関連の実装だけを入れた最小構成を用意します。
- ここで同じ症状が出れば「環境・Hub 設定・証明書・ペイロード」の可能性が高く、出なければ「既存プロジェクト固有(Manifest / Capabilities / DI / 画面遷移)」の可能性が高いと判断できます。
- 最小構成では、まず受信したことをログに出すだけにし、通知表示(ローカル通知)などの要素は後回しにします。
デバッグ実行だけで判断しない
デバッグ中はバックグラウンド制限や最適化が変わったり、デバッガ接続の状態で受信タイミングが変わったりします。次のパターンで挙動が一致するか確認します。
- デバッグ実行(USB 接続 / ワイヤレス)
- 端末上のアイコンから手動起動(デバッガ無し)
- Release ビルドの配布(Android は内部共有、iOS は TestFlight など)
ログを取る場所を固定する
| 場所 | 確認方法 | 見るべき内容 |
|---|---|---|
| Android 端末 | logcat | FCM 受信(OnMessageReceived)、トークン更新(OnNewToken)、権限エラー、通知チャンネル関連 |
| iOS 端末 | Xcode のデバイスログ | 登録成功/失敗、Push 許可状態、APNs からの受信イベント、ペイロード解析エラー |
| Azure Notification Hubs | 診断ログ(Azure Monitor) | 送信要求、プロバイダーへの応答、失敗理由(無効トークン、ペイロード不正など)の手がかり |
この段階で「どこまで到達しているか」が見えれば、修正は速くなります。次から Android 側の典型パターンを潰します。
Android で届かないときの最短ルート:受信サービス・権限・通知チャンネル
Android は「トークンは取れているのに届かない」「Hub は成功なのに届かない」が起きやすい一方で、修正ポイントが比較的はっきりしています。MAUI 移行で差分になりやすいのは、主に次の3点です。
- FCM を受け取る FirebaseMessagingService 相当が正しく登録されている
- Android 13+ の 通知ランタイム権限(POST_NOTIFICATIONS)が許可されている
- 通知を表示する場合は 通知チャンネルが作成され、ユーザー設定でブロックされていない
FirebaseMessagingService 相当が存在し、OnMessageReceived に入れているか
.NET MAUI では Xamarin のときとプロジェクト構成が変わるため、受信サービスが「作ったつもりでも読み込まれていない」状態が起きます。まずは受信できたら必ずログが出る最小コードを入れます(通知表示は後回し)。
using Android.App;
using Android.Util;
using Firebase.Messaging;
namespace YourApp.Platforms.Android;
[Service(Exported = false)]
[IntentFilter(new[] { "com.google.firebase.MESSAGING_EVENT" })]
public class AppFirebaseMessagingService : FirebaseMessagingService
{
public override void OnNewToken(string token)
{
base.OnNewToken(token);
Log.Info("FCM", $"OnNewToken: {token}");
// TODO: ここで Azure Notification Hubs への登録処理に渡す
// 例)端末識別タグと一緒に installation / registration を更新する
}
public override void OnMessageReceived(RemoteMessage message)
{
base.OnMessageReceived(message);
Log.Info("FCM", $"From: {message.From}");
Log.Info("FCM", $"Data keys: {string.Join(\",\", message.Data?.Keys ?? new List<string>())}");
// TODO: フォアグラウンド中に表示したいならローカル通知を出す、など
// まずは "受信できた" を確認するのが最優先
}
}
このログが一切出ない場合は、FCM が受信できていないか、サービスが正しく登録されていない可能性が高いです。まず OnNewToken が出るか(= Firebase 初期化とサービス登録が最低限動いているか)から確認すると迷いません。
Android 13+ の POST_NOTIFICATIONS を Manifest とランタイムの両方で許可する
Android 13(API 33)以降は、通知の表示にランタイム権限が必要です。Manifest に追加しただけでは足りず、初回起動時などでユーザーに許可を求める必要があります。
AndroidManifest.xml(例)
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
MainActivity などでの権限要求(例)
using Android;
using Android.Content.PM;
using Android.OS;
if (Build.VERSION.SdkInt >= BuildVersionCodes.Tiramisu)
{
if (CheckSelfPermission(Manifest.Permission.PostNotifications) != Permission.Granted)
{
RequestPermissions(new[] { Manifest.Permission.PostNotifications }, 1001);
}
}
よくある落とし穴として、アプリ側で許可ダイアログを出していなくても「Firebase コンソールから送ると届く」ように見えるケースがあります。これは通知が端末に届いていても、OS が表示をブロックしているだけ、という状態です。端末の「アプリ通知設定」も必ず確認してください。
通知チャンネルが無い/重要度が低いと「届いているのに見えない」
Android 8(API 26)以降、通知を表示するにはチャンネルが必要です。アプリ内でローカル通知を出す場合も同様です。さらに、チャンネルの重要度が低いとバナーが出ず「無反応」に見えることがあります。
| 項目 | 期待値 | ミスしやすい点 |
|---|---|---|
| POST_NOTIFICATIONS | Manifest 追加 + ランタイム許可済み | 許可要求を実装していない/端末設定で拒否 |
| FirebaseMessagingService | OnNewToken / OnMessageReceived がログに出る | IntentFilter の不足/namespace の場所違い/ビルド条件で除外 |
| 通知チャンネル | 存在し、重要度が適切 | チャンネル未作成/重要度 LOW/ユーザーが個別に無効化 |
| バックグラウンド制限 | 最適化の例外が不要な設計 | 特定端末で強制停止・省電力が強く、遅延や欠落が起きる |
Android は、Manifest / MainActivity の不足を補うだけで一気に改善することがあります。ここまでで受信ログが取れるようになったら、次は Hub 側の「登録」と「ペイロード」を疑います。
Azure Notification Hubs 側の確認:FCM v1 設定・トークン登録・タグ設計
Firebase コンソールからは届くのに、Hub 経由だと届かない場合は、端末側よりもHub 側の「資格情報」「登録」「送信形式」にズレがあることが多いです。特に FCM v1(HTTP v1 / サービスアカウント)へ移行している場合、設定ミスがあっても表面上は分かりづらいのが厄介です。
FCM v1 の前提が揃っているか
- Hub が FCM v1 で構成され、Firebase のサービスアカウント情報(権限含む)が正しく登録されている
- サービスアカウントが紐づく Firebase プロジェクトと、アプリが使っている Firebase プロジェクトが一致している
- 送信対象のトークンが、その Firebase プロジェクトで発行された最新の登録トークンである
「Xamarin 時代は動いていた」場合でも、MAUI 移行でアプリの ApplicationId(パッケージ名)や Bundle IDが変わっていると、Firebase 側のプロジェクトや構成ファイル(google-services.json / GoogleService-Info.plist)との整合が崩れます。まずはアプリが参照している Firebase プロジェクトがどれかを固定し、Hub 側も同じプロジェクトを向いているか確認します。
Hub に登録しているトークンが「今その端末のトークン」かを突き合わせる
最も多い原因がこれです。FCM トークンは端末・インストール状況・アプリ署名・環境で変わることがあり、古いトークンへ送っても何も起きません。対策はシンプルで、次を徹底します。
- 端末で取得したトークンを必ずログに出す(Android の OnNewToken / 初回取得時)
- Hub への登録処理で、ログに出したトークンがそのまま登録に使われていることを確認する
- 複数端末・複数ユーザーを扱うなら、タグ(例:userId、tenantId、環境)を付け、どのタグへ送っているかを追えるようにする
| 確認観点 | チェック方法 | 問題があった場合の典型症状 |
|---|---|---|
| トークンの一致 | 端末ログのトークンと、Hub 登録時のトークンを比較 | Hub は成功表示、端末は無反応(最頻出) |
| 登録の更新 | OnNewToken 発火時に必ず Hub 側も更新 | 初回だけ届く/端末変更後に届かない |
| タグ設計 | 送信先タグと登録タグを一覧で見直す | 特定ユーザーだけ届かない/環境で混線する |
ペイロードの違いで詰まりやすい:まずは data-only で「アプリに届くか」を見る
通知の見た目(バナー表示)まで一気に確認しようとすると、Android 13 の権限や通知チャンネルなど別要因が混ざります。そこで最初はdata-only(データ専用)で「アプリの受信処理に入るか」を確認すると、原因箇所の切り分けが一気に楽になります。
data-only の例(Android / FCM)
{
"data": {
"title": "Test",
"body": "This is a test notification"
}
}
この状態で OnMessageReceived のログが出れば、FCM 受信ルートは生きています。次に「通知として表示する」要素(notification ブロック、チャンネル、権限)を足していくと、どこで破綻するかが明確になります。
Hub からの送信は「プラットフォームごとに期待する形式が違う」
Notification Hubs はマルチプラットフォームを扱える反面、送信する JSON はFCM 用と APNs 用で形が違うのが基本です。Android 向けに作った JSON をそのまま iOS に投げると「ペイロードが無効」になりやすいので、送信側で次のいずれかを選びます。
- プラットフォーム別に別ペイロードを送る(Android は FCM 形式、iOS は APNs 形式)
- テンプレート通知を使い、各プラットフォームのテンプレートへ同じ論理データを流し込む
特に移行直後は、まず「プラットフォーム別に最小ペイロードで届く」ことを確認し、その後にテンプレート化する流れが失敗しづらいです。
iOS だけ届かない/「ペイロードが無効」になるとき:aps と署名・エンタイトルメント
Android が直ったあとに iOS で詰まるケースはかなり多く、原因は大きく分けてペイロード形式と署名/環境の不一致です。iOS は APNs が厳格で、少しでも条件が合わないと端末まで届きません。
APNs 形式は aps オブジェクトが必須
iOS へ送る場合、基本は APNs の仕様に従い、ルートに aps オブジェクトを含めます。これが無いと「ペイロードが無効」と扱われやすくなります。
APNs 形式の例(iOS)
{
"aps": {
"alert": {
"title": "Test",
"body": "This is a test notification"
},
"sound": "default"
}
}
逆に、Android(FCM)向けの data をルートに置いた JSON をそのまま iOS に送ると、APNs では理解できず失敗します。送信側で「Android と iOS で JSON を分ける」か、「テンプレートで吸収する」設計に寄せると再発しにくくなります。
aps-environment(development / production)がプロビジョニングと一致しているか
iOS のプッシュ通知は、ビルドの署名状態によって APNs の環境が変わります。ここがズレると、送っても届きません。
- Debug(開発用署名)のビルドなのに、Hub 側が Production 用の資格情報を使っている
- Release(配布用署名)のビルドなのに、Hub 側が Development 用の資格情報を使っている
.NET MAUI では Entitlements.plist に aps-environment を設定します。さらに、Apple Developer 側の App ID で Push Notifications が有効であること、利用しているプロビジョニングプロファイルがそれを含んでいることも必要です。
| 環境 | 典型的な配布形態 | aps-environment | Hub 側の想定 |
|---|---|---|---|
| 開発 | ローカル実機デバッグ | development | APNs(開発)に送れる資格情報 |
| 本番 | TestFlight / App Store | production | APNs(本番)に送れる資格情報 |
Notification Hubs の APNs 資格情報がアプリ署名と一致しているか
Hub で APNs を設定する際、証明書方式(p12)かトークン方式(p8)かに関わらず、次が一致している必要があります。
- Bundle ID(アプリ識別子)が一致している
- 有効期限が切れていない(証明書方式の場合)
- チーム・キー情報が正しい(トークン方式の場合)
- 送信している環境(開発/本番)が一致している
iOS 側の受信実装:通知許可の要求と登録ができているか
iOS はユーザーの許可が無いと表示できません。また、許可を取っていても、APNs のデバイストークン登録(RegisterForRemoteNotifications)が走っていないと受信できません。MAUI 移行で AppDelegate 周りを整理した際に、ここが抜けることがあります。
using Foundation;
using UIKit;
using UserNotifications;
namespace YourApp.Platforms.iOS;
[Register("AppDelegate")]
public class AppDelegate : MauiUIApplicationDelegate
{
public override bool FinishedLaunching(UIApplication app, NSDictionary options)
{
UNUserNotificationCenter.Current.RequestAuthorization(
UNAuthorizationOptions.Alert | UNAuthorizationOptions.Badge | UNAuthorizationOptions.Sound,
(approved, error) =>
{
// approved が false の場合はユーザーが拒否している
});
UIApplication.SharedApplication.RegisterForRemoteNotifications();
return base.FinishedLaunching(app, options);
}
public override void RegisteredForRemoteNotifications(UIApplication application, NSData deviceToken)
{
// deviceToken を文字列化してログに出す → Hub 登録に使用
var bytes = deviceToken.ToArray();
var token = BitConverter.ToString(bytes).Replace("-", "");
Console.WriteLine($"APNs Device Token: {token}");
}
public override void FailedToRegisterForRemoteNotifications(UIApplication application, NSError error)
{
Console.WriteLine($"APNs registration failed: {error}");
}
}
ここで重要なのは、「許可ダイアログが出たか」ではなく「登録に成功してデバイストークンを得られたか」です。デバイストークンが取れない限り、Hub の登録も正しくできません。
iOS チェック表:ペイロードと設定のどちらが原因か切り分ける
| チェック項目 | OK の目安 | NG だと起きること |
|---|---|---|
| 通知許可 | 端末設定で通知が許可されている | 届いても表示されない |
| APNs 登録 | デバイストークンが取得できる | 端末に一切届かない |
| aps-environment | development / production が署名と一致 | Hub は成功でも届かない |
| 送信ペイロード | ルートに aps があり、JSON が妥当 | 「ペイロードが無効」になりやすい |
| Hub の APNs 資格情報 | Bundle ID・期限・環境が一致 | 送信エラー/届かない |
実務では、iOS は「まず aps を入れた最小ペイロードで届く」状態を作り、その後でカスタムデータやディープリンクなどを追加するのが最短です。
テスト送信の定石:原因を混ぜない確認順
プッシュ通知は「届かない理由」が複数同時に存在しがちです。そこで、テストは一度に要素を増やさないのが鉄則です。次の順に確認すると、遠回りを避けられます。
| 目的 | 送信先 | ペイロードのポイント | 期待する観測 |
|---|---|---|---|
| 「クラウド→端末」経路が生きているか | 単一端末(トークン直指定 / 単一タグ) | 最小構成(Android: data-only / iOS: aps のみ) | 端末ログに受信イベントが出る |
| アプリ側処理が正しいか | 同上 | data にキーを増やす(deep link など) | データ解析が成功し、画面遷移などが期待通り |
| 「表示」要素の問題を切り分ける | 同上 | Android: 通知チャンネル / 権限、iOS: alert/sound/badge | バナー・通知センターに表示される |
| 配信対象の設計が正しいか | 複数端末・ユーザー | タグ設計、テンプレート通知、環境(dev/prod)の分離 | 狙ったユーザーだけ届く(混線しない) |
本番プロジェクトへ戻すときの移植チェックリスト
最小構成で受信できたら、その設定を本番プロジェクトへ「丸ごと移植」するのが王道です。移植時に差分が出やすい項目をチェックリスト化します。
| 領域 | チェック項目 | 意図 |
|---|---|---|
| Android | FirebaseMessagingService / POST_NOTIFICATIONS / チャンネル作成 | 受信と表示の最低条件を満たす |
| Android | トークン取得ログ → Hub 登録更新 | トークン不一致を根絶する |
| Hub | FCM v1 資格情報・プロジェクト一致 | 「別プロジェクトへ送っていた」を防ぐ |
| Hub | タグ設計(環境・ユーザー・端末種別) | 配信対象の混線を防ぐ |
| iOS | Push Notifications 有効化 / aps-environment / 許可要求 | APNs 登録と受信の前提を満たす |
| iOS | 送信ペイロードは aps を含む | 「ペイロードが無効」を防ぐ |
まとめ:移行後に詰まったら「端末→Hub→ペイロード」の順に戻る
今回のように Xamarin から .NET MAUI へ移行した直後は、Android は Manifest / MainActivity 周りの不足で受信できず、iOS は APNs の aps を含まないペイロードや環境不一致で弾かれる、という流れになりがちです。まずは最小構成で「受信ログが出る」状態を作り、Android は権限と受信サービス、iOS は aps と署名設定を揃えたうえで、本番プロジェクトへ設定を移植すると安定して解決できます。

コメント