Xamarin から .NET MAUI 移行後に Azure Notification Hubs のプッシュ通知が届かない原因と対処法(FCM v1 / Android・iOS)

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 端末logcatFCM 受信(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_NOTIFICATIONSManifest 追加 + ランタイム許可済み許可要求を実装していない/端末設定で拒否
FirebaseMessagingServiceOnNewToken / 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-environmentHub 側の想定
開発ローカル実機デバッグdevelopmentAPNs(開発)に送れる資格情報
本番TestFlight / App StoreproductionAPNs(本番)に送れる資格情報

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-environmentdevelopment / 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)の分離狙ったユーザーだけ届く(混線しない)

本番プロジェクトへ戻すときの移植チェックリスト

最小構成で受信できたら、その設定を本番プロジェクトへ「丸ごと移植」するのが王道です。移植時に差分が出やすい項目をチェックリスト化します。

領域チェック項目意図
AndroidFirebaseMessagingService / POST_NOTIFICATIONS / チャンネル作成受信と表示の最低条件を満たす
Androidトークン取得ログ → Hub 登録更新トークン不一致を根絶する
HubFCM v1 資格情報・プロジェクト一致「別プロジェクトへ送っていた」を防ぐ
Hubタグ設計(環境・ユーザー・端末種別)配信対象の混線を防ぐ
iOSPush Notifications 有効化 / aps-environment / 許可要求APNs 登録と受信の前提を満たす
iOS送信ペイロードは aps を含む「ペイロードが無効」を防ぐ

まとめ:移行後に詰まったら「端末→Hub→ペイロード」の順に戻る

今回のように Xamarin から .NET MAUI へ移行した直後は、Android は Manifest / MainActivity 周りの不足で受信できず、iOS は APNs の aps を含まないペイロードや環境不一致で弾かれる、という流れになりがちです。まずは最小構成で「受信ログが出る」状態を作り、Android は権限と受信サービス、iOS は aps と署名設定を揃えたうえで、本番プロジェクトへ設定を移植すると安定して解決できます。

この記事を書いた人

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

コメント

コメントする

目次