Firebase iOS Push Notifications: APNs Key and SDK Setup
Firebase Cloud Messaging (FCM) can deliver push notifications to iPhones, but it doesn't talk to iOS devices directly — it goes through Apple Push Notification service (APNs). Most “Firebase iOS push isn't working” problems come from the Apple side of that handshake. This guide sets up both sides in order.
How Firebase push works on iOS
Your app registers with APNs and receives an APNs device token. The Firebase iOS SDK maps that token to an FCM registration token, which is the one your server uses. When you send a message, FCM authenticates with APNs using the key you uploaded, and APNs delivers it to the device. If Firebase can't authenticate with APNs, nothing arrives — even though everything looks fine in your app.
Prerequisites
- A paid Apple Developer Program membership — push needs one.
- A Firebase project with your iOS app added, and its GoogleService-Info.plist.
- Xcode and a physical iPhone or iPad. The Simulator isn't a reliable test for remote push.
1. Create an APNs authentication key
1. In the Apple Developer portal, open Certificates, Identifiers & Profiles → Keys and create a new key.
2. Enable Apple Push Notifications service (APNs) for the key, then register it.
3. Download the .p8 file and note its Key ID. Apple lets you download it only once, so store it somewhere safe.
4. Note your Team ID, shown in your developer account's membership details.
An authentication key doesn't expire like a push certificate, and one key works for all your apps — which is why it's the recommended option over the older .p12 certificates.
2. Upload the key to Firebase
In the Firebase console, open Project settings → Cloud Messaging. Under your Apple app, upload the .p8 key and enter the Key ID and Team ID. Firebase uses this key to authenticate with APNs on your behalf.
3. Add the Firebase iOS SDK
In Xcode, choose File → Add Package Dependencies and add https://github.com/firebase/firebase-ios-sdk. Select the FirebaseMessaging product. Then drag GoogleService-Info.plist into your app target.
4. Enable capabilities
Select your app target, open Signing & Capabilities, and add Push Notifications. Add Background Modes and check Remote notifications if you send silent or data-only pushes.
5. Request permission and get the FCM token
Configure Firebase at launch, ask the user for permission, register for remote notifications, and listen for the FCM token:
import UIKit
import FirebaseCore
import FirebaseMessaging
import UserNotifications
@main
class AppDelegate: UIResponder, UIApplicationDelegate,
UNUserNotificationCenterDelegate, MessagingDelegate {
func application(_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions:
[UIApplication.LaunchOptionsKey: Any]?) -> Bool {
FirebaseApp.configure()
Messaging.messaging().delegate = self
UNUserNotificationCenter.current().delegate = self
UNUserNotificationCenter.current().requestAuthorization(
options: [.alert, .badge, .sound]
) { granted, error in
// Respect the user's choice; you can ask again later from settings
}
application.registerForRemoteNotifications()
return true
}
// Called with a new or refreshed FCM registration token
func messaging(_ messaging: Messaging,
didReceiveRegistrationToken fcmToken: String?) {
guard let fcmToken else { return }
// Send this token to your push server
print("FCM token: \(fcmToken)")
}
// Show notifications while the app is in the foreground
func userNotificationCenter(_ center: UNUserNotificationCenter,
willPresent notification: UNNotification) async
-> UNNotificationPresentationOptions {
[.banner, .sound, .badge]
}
}
By default the Firebase SDK uses method swizzling to pass the APNs token to FCM automatically. If you set FirebaseAppDelegateProxyEnabled to NO in your Info.plist, you must pass the APNs token yourself by setting Messaging.messaging().apnsToken in didRegisterForRemoteNotificationsWithDeviceToken.
6. Send a test notification
Run the app on a physical device, accept the permission prompt, and copy the FCM token from the console. In the Firebase console, open Messaging, create a notification, and use Send test message with that token. Test with the app in the foreground, in the background, and fully closed.
Why Firebase iOS push isn't working
- No APNs key in Firebase: the token is generated, but nothing is delivered. Check Project settings → Cloud Messaging.
- Wrong Team ID or Key ID: APNs rejects Firebase's requests. Re-enter both from your Apple developer account.
- Bundle ID mismatch: the iOS app in Firebase must use exactly the same bundle ID as your Xcode target.
- Push Notifications capability missing: the app never receives an APNs token, so no FCM token works.
- Swizzling disabled without passing the APNs token: FCM has no APNs token to map.
- Permission denied: the user declined the prompt; notifications are delivered silently or not shown.
Using React Native or Flutter?
The Apple-side steps — APNs key, Firebase upload, and capabilities — are the same. The notifySphere SDKs handle the token and permission code for you: see the React Native guide and the Flutter guide. notifySphere runs on Firebase Cloud Messaging, so once your Firebase project can reach APNs, uploading its service account lets you send segmented iOS, Android, and web campaigns from one dashboard.
---
Originally published on the notifySphere blog. notifySphere is push notifications, WhatsApp Business API, deep linking, and in-app support in one SDK — free up to 1,000 users.
Swift Package ManagerをTuist経由で導入し、CocoaPodsと併用しながら移行する
株式会社DIGITALIOで開発しているiOS版 ECナビアンケートアプリ は、依存関係の管理にCocoaPodsを使っていました。本記事では、依存関係の管理をCocoaPodsからSPM(Swift Package Manager)に移行した経緯と手法について書きます。
TL;DR
想定読者: Tuistでプロジェクトを生成していて、CocoaPodsからSPMへの移行を検討している方
依存関係の宣言をTuistの定義ファイルに集約でき、移行は「定義ファイルへの追加と Podfile からの削除」の繰り返しで済んだ
広告メディエーションのSDK本体とアダプターだけCocoaPodsに残し、SPMと併用で運用できている
前提
本記事は、TuistでXcodeプロジェクトを生成している環境を前提にしています。前回の記事では、 project.pbxproj のコンフリクトをなくすためにTuistを導入し、ビルドターゲットを Project.swift に定義して tuist generate で生成する運用に切り替えました。
zenn.dev
生成物はGit管理から除外しているため、ブランチの切り替えやファイルの追加のたびに make sync-develop でプロジェクトを再生成しています。
SPM移行のきっかけ
きっかけはFirebaseから届いたメールでした。Firebase SDKは2026年10月にCocoaPodsへの新バージョン公開を停止し、以降はpodspecからのインストールが公式サポートの対象外となり、SPMまたは手動インストールへの移行が案内される、という内容です。手順は公式の移行ガイドにまとまっています。
firebase.google.com
これはFirebase固有の話ではありません。CocoaPods Trunk自体も、2026年12月2日にread-onlyモードへ移行することが以前から告知されており、同じ流れにあります1。
blog.cocoapods.org
CocoaPods経由でFirebaseを使い続けても公開済みのバージョンは動きますが、新機能やバグ修正は受け取れなくなります2。この告知を機に、依存関係管理をCocoaPodsからSPMへ移行することにしました。
依存関係を種類ごとに分けて移行
まず、Tuist経由でSPMを利用するために、依存関係を記述する Tuist/Package.swift を作成しました。
// swift-tools-version: 5.9
import PackageDescription
#if TUIST
import ProjectDescription
let packageSettings = PackageSettings(
productTypes: [:]
)
#endif
let package = Package(
name: "ECNaviEnqueteApp",
dependencies: [
// ここに追記していく
]
)
次に、 Podfile に書かれていた依存関係を、ネットワーク、認証、UI、その他の依存関係といった種類ごとに分類して順番に移行しました。種類ごとに分けたのは、1つのPRで動作確認すべき範囲を絞り、問題が起きたときに原因を特定しやすくするためです。
どのタスクも手順自体は共通で、次の3ステップです。実際にFirebaseを移行した例で見てみます。
Tuist/Package.swift に対象の依存関係を追加する
// ...
- .package(
- url: "firebase/firebase-ios-sdk",
- exact: "12.14.0"
- ),
.package(
url: "kaishin/Gifu",
exact: "3.4.0"
),
Project.swift で .external(name:) として依存関係を宣言する
// ...
- .external(name: "FirebaseAnalytics"),
- .external(name: "FirebaseMessaging"),
- .external(name: "FirebaseRemoteConfig"),
- .external(name: "FirebaseInAppMessaging-Beta"),
- .external(name: "FirebaseCrashlytics"),
.external(name: "Gifu"),
CocoaPodsではsubspec( Firebase/InAppMessaging など)で選んでいた機能が、SPMでは1つのパッケージが公開する複数のプロダクトになります。
.external(name:) にはSPM側のプロダクト名を指定するため、Pod側の名前と異なるものもあります。
Podfile から該当のPodを削除する
- # Firebase
- pod 'Firebase/Core', '>10.22.0'
- pod 'Firebase/Analytics'
- pod 'Firebase/Messaging'
- pod 'Firebase/RemoteConfig'
- pod 'Firebase/InAppMessaging'
- pod 'Firebase/Crashlytics'
-
...
依存関係の解決はTuistが担うようになるため、 Makefile に tuist install を追加しました。依存関係の解決はプロジェクト生成時に済んでいる必要があるため、 tuist generate の前に置いています。
sync-develop:
cp ECNaviEnqueteApp/Resource/GoogleService-Info-Develop.plist ECNaviEnqueteApp/Resource/GoogleService-Info.plist
- tuist install
tuist generate --no-open
bundle exec pod install
変更を反映するには make sync-develop を実行します。次の順にコマンドが動き、SPMとCocoaPodsのロックファイルも更新されます。
移行でつまずいた点と残った課題
SPMへの移行では、単に依存関係の宣言先を変えるだけでなく、依存関係の利用方法によって個別対応が必要になったものもありました。
スクリプトのパスが変わった
移行した依存関係の1つであるFirebase Crashlyticsは、ビルド後にdSYMをアップロードするビルドスクリプト(Run Script)を実行します。
このスクリプトは、CocoaPodsでは Pods/ 配下にありましたが、SPMでは取得したパッケージのディレクトリ内に移ります。Tuistはこれを Tuist/.build/checkouts/ 配下に置くため、移行のタイミングで定義ファイルのスクリプトが参照するパスも変更しました。
- "${PODS_ROOT}/FirebaseCrashlytics/run"
- "${SRCROOT}/Tuist/.build/checkouts/firebase-ios-sdk/Crashlytics/run"
コードの追加対応を伴ったもの
依存関係によっては、コードの追加が必要になった箇所もありました。例えば、ネットワークレイヤーを抽象化するMoyaをCocoaPodsからSPMに移行してビルドし直すと、次のようなエラーが出ました。
FooBarClass.swift:xx:yy Cannot find 'UIDevice' in scope
原因は、CocoaPodsが各Podに生成するumbrella header( Moya-umbrella.h )とmodulemapでした。新規のプロジェクトで Podfile に use_frameworks! を指定してMoyaを導入すると、次のようなファイルが生成されます(抜粋)。
// Moya-umbrella.h
#ifdef OBJC
#import <UIKit/UIKit.h>
#else
// ...
// Moya.modulemap
framework module Moya {
umbrella header "Moya-umbrella.h"
export *
module * { export * }
}
umbrella headerがUIKitを、UIKitがFoundationを読み込み、modulemapの export * がそれらを再エクスポートします。そのため、 import Moya と書くだけでFoundationとUIKit双方のシンボルが使えていました。
import Moya
│
├─ CocoaPods時代: FoundationもUIKitもついてきた ٩(ˊᗜˋ*)و
│
└─ SPM: Moyaしか来ない
(;゚Д゚) 「UIDeviceって誰…?」
SPM経由ではこの経路が失われます。その結果、実際にはFoundationやUIKitのAPIを直接使っているのに、Moyaのimportだけに依存していた数ファイルがコンパイルエラーになりました。
対象ファイルに import Foundation / import UIKit を追加して解決しました。暗黙の再エクスポートは他のPodでも起きうるため、移行対象を分けておくと、コンパイルエラーの原因を直前に移したPodに絞り込めます。
一部の依存関係はSPM対応待ち
すべての依存関係をSPMへ移行できたわけではありません。広告配信のために利用している広告メディエーションでは、SDK本体こそSPM対応が完了していました。しかし、私たちが利用している複数の広告ネットワークに対応するアダプターは、CocoaPods版のSDK本体に依存する構成でした。
アダプターのPodはSDK本体のPodを依存関係として引き込むため、SDK本体だけSPMへ移すと、同じSDKがCocoaPods版とSPM版の2系統で宣言された状態になります。検証したところ、ビルドは通りました。
ただし版が異なる場合、どちらが採用されるかを示す警告は出ず、手元の検証ではSPM版が採用されました3。意図しない版が混ざる状態を避けるため、アダプターのSPM対応が済むまでは、SDK本体もCocoaPodsに残すこととしました。
開発ツールはmiseで管理
本アプリでは、Swiftの静的解析ツールであるSwiftLintと、アプリで使用しているオープンソースライブラリのライセンス一覧を自動生成するLicensePlistを使用しています。
これらは、アプリ本体にリンクされるわけではない開発ツールです。CocoaPods管理から切り出すにあたって、前回Tuistのバージョン固定用に導入したmiseへ寄せ、ツールのバージョン管理を一元化しました。
[tools]
- license-plist = "3.27.9"
- swiftlint = "0.63.3"
tuist = "4.195.9"
これらを呼ぶスクリプトは、 Pods/ 配下のパスからmiseのshimを指すように変更しました。
- "${PODS_ROOT}/SwiftLint/swiftlint"
- "${MISE_DATA_DIR:-${HOME}/.local/share/mise}/shims/swiftlint"
なお、LicensePlistは、SPM管理のパッケージを検出するための設定が必要でした。LicensePlistは、SPMのパッケージを Package.resolved から探します。しかし本アプリでは、Tuistが Tuist/Package.resolved を管理しており、LicensePlistの既定の探索先には存在せず、アプリ内のライセンス一覧に含まれませんでした4。
そこで、 license_plist.yml の packagePaths に Tuist/Package.swift を指定すると、同じディレクトリの Package.resolved が参照されるようになりました。
- options:
- packagePaths:
- - Tuist/Package.swift
移行後の状況
一連の移行の結果、依存関係の状態は次のようになりました。
状態
対象
SPMへ移行済み
ネットワーク、認証、UI、その他の依存関係
miseへ移行済み
SwiftLint、LicensePlist
CocoaPodsのまま維持
広告メディエーションSDK本体とアダプター
Podfile に書かれていたSDK本体とアダプター以外の21件のPod宣言は、すべてなくなりました。残りの依存関係は、提供元のリリースノートでSPM対応の有無を確認しながら追従していきます。
まとめ
Tuistの .external(name:) で依存関係を宣言できるため、移行作業の多くは定義ファイルへの追加と Podfile からの削除という小さな差分の繰り返しになりました。種類ごとに分けて、独立してレビュー・マージできる単位で進めたことで、各PRも小さく保てました。同様の移行を控えている場合は、以下の手順で実施するとスムーズに移行が進められると思います。
前回の記事 を参考にXcodeプロジェクトの管理方式を決める。
依存関係ごとに、SPMへの対応状況を調べる。
種類ごとに分け、動作確認しやすい粒度で順番に移行する。
CocoaPodsに残る依存関係は、12月2日以降は新バージョンがTrunkに公開されなくなります。提供元のSPM対応状況を確認し、未対応のものはアダプターごと移行できる時期を見極める必要があります。
脚注
read-only化後も既存のPodは取得できるが、Trunkに新しいPodやバージョンは公開できなくなる。独自のspecリポジトリで配布されているPodは影響を受けない。 ↩︎
Firebase公式の移行ガイドによれば、CocoaPodsで最後に公開されるメジャーバージョンは12とされている。 ↩︎
CocoaPodsとSPMのバージョンを入れ替えて2通りで検証した。いずれも、ビルドしたバイナリに含まれていたのはSPM版で、新旧は関係なかった。どのパターンでも同じかまでは検証できていない。 ↩︎
既定の探索先は、プロジェクトルートの Package.swift と、 *.xcodeproj および *.xcworkspace 配下の xcshareddata/swiftpm/Package.resolved である。詳細は LicensePlistのREADME を参照。 ↩︎
firebase-ios-sdk#16972 Firebase v13からSceneDelegateを考慮するようになってFIAMのactionURLがhttpsだとどんなURLもUniversalLinkとして流れてくるのが困るよーというIssue
2 likes · 94 views
Google's postmortem (October 2, 2026):
firebase.blog
The GitHub issue that tracked the outage while the status pages stayed green:
firebase-ios-sdk#16728
The Facebook SDK crash of May 2020:
facebook-ios-sdk#1373
1 like · 275 views
11/ Google's postmortem (October 2, 2026):
firebase.blog
The GitHub issue that tracked the outage while the status pages stayed green:
firebase-ios-sdk#16728
CocoaPods Is Going Read-Only: Migrating a 20+ Dependency iOS App to Swift Package Manager
Your Podfile still installs. That is exactly why this is easy to postpone — and why a project with twenty-plus dependencies should start now rather than in December.
First, what actually changes
CocoaPods trunk is moving to a read-only state. In the official words:
> "December 2nd 2026 — I will switch trunk to not accept new Podspecs permanently."
And from Firebase:
> "Firebase 12 is the final major version published to CocoaPods, and Firebase will stop publishing new versions to CocoaPods in October 2026."
Read-only is not shut down. Existing pod versions stay downloadable, the Specs repo and CDN keep serving, and pod install keeps resolving what it resolves today. CocoaPods is explicit about this: "Making the switch will not break builds for people using CocoaPods in 2026 onwards." Firebase says the same about its own packages: existing versions "in CocoaPods will remain installable and functional as long as CocoaPods ecosystem support continues."
What you lose after December is the ability to receive new versions through trunk. For an app that ships quarterly, that is a slow, undramatic problem — the worst kind, because nothing pages you at 3 a.m.
Timeline
| When | What happens |
|---|---|
| 2024-11-30 | CocoaPods announces the plan: "In two years we plan to turn CocoaPods trunk to be read-only." |
| Mid–late 2025 | First email to everyone who contributed a Podspec |
| Sep–Oct 2026 | Second email, roughly one month before the test run |
| Oct 2026 | Firebase stops publishing to CocoaPods; Firebase 12 is the last major version |
| Nov 1–7, 2026 | Test run of read-only, "giving automation a chance to break early" |
| Dec 2, 2026 | Trunk permanently stops accepting new Podspecs |
| After | Existing versions remain installable; Specs repo and CDN keep serving |
CocoaPods adds a caveat worth quoting: "These dates are not set in stone." Treat the December date as a planning deadline, not a hard API cutoff. Flutter has already moved in the same direction — starting with Flutter 3.44, Swift Package Manager replaces CocoaPods as the default for iOS and macOS apps — so the direction of travel is not in question.
Step 1: Inventory and freeze
Do this on a branch, and commit the current working state before you touch anything.
cd "$(git rev-parse --show-toplevel)"
git checkout -b spm-migration
grep -nE "^\s*pod " Podfile | tee /tmp/pods.txt
wc -l < /tmp/pods.txt
git add Podfile Podfile.lock
git commit -m "chore: snapshot CocoaPods state before SPM migration"
Then go through /tmp/pods.txt line by line and mark each dependency: has a Swift package, has a binary xcframework, or has neither. The third bucket decides your schedule. Twenty pods where eighteen migrate cleanly is a week; three pods with no package support can eat a month.
One thing people miss: CocoaPods subspecs do not map one-to-one onto Swift packages. A pod like Vendor/Core and Vendor/UI may become two products of a single package, or one product with everything included. Read the vendor's own SPM instructions.
Step 2: Deintegrate
Quit Xcode first. pod deintegrate rewrites the project file, and an open Xcode window will happily overwrite those changes when it saves.
pod deintegrate
pod clean
mkdir -p archive/cocoapods
git mv Podfile archive/cocoapods/Podfile
git mv Podfile.lock archive/cocoapods/Podfile.lock
rm -rf Pods *.xcworkspace
git status --short
Archive, do not delete. The Podfile.lock is your rollback artifact for the rest of the migration.
Step 3: Add the packages
For an app target, the path of least resistance is Xcode: open the .xcodeproj, choose File → Add Package Dependencies, paste the repository URL, pick the products, and confirm they are added to the target's "Frameworks, Libraries, and Embedded Content."
If your app is split into local Swift packages, declare the dependency in Package.swift instead:
// swift-tools-version: 6.0
import PackageDescription
let package = Package(
name: "AppCore",
platforms: [.iOS(.v17)],
dependencies: [
.package(
url: "https://github.com/firebase/firebase-ios-sdk",
from: "12.0.0"
)
],
targets: [
.target(
name: "AppCore",
dependencies: [
.product(name: "FirebaseAuth", package: "firebase-ios-sdk"),
.product(name: "FirebaseFirestore", package: "firebase-ios-sdk")
]
)
]
)
Commit Package.resolved alongside Package.swift. It is the SPM equivalent of Podfile.lock, and without it your CI and your laptop can resolve different revisions of the same branch-based dependency.
Step 4: Clean the leftovers
CocoaPods leaves more behind than the Pods/ directory. Check for:
grep -n "PODS_ROOT\|Pods/" App.xcodeproj/project.pbxproj
You are looking for build settings pointing at ${PODS_ROOT}, and build phases named [CP] Check Pods Manifest.lock, [CP] Embed Pods Frameworks, and [CP] Copy Pods Resources. Remove them.
Keep the Crashlytics dSYM upload run script, but note that Firebase documents a different script path for SPM checkouts than for CocoaPods installs. Copy the current version from the Crashlytics setup page rather than editing the old path by hand. A stale path here fails silently: uploads just stop, and you find out weeks later when a crash report has no symbols.
Step 5: CI
The failure mode in CI is almost always cache, not code. Old caches still contain a Pods/ directory and a stale workspace that no longer exists in the repo.
- name: Resolve packages
run: |
rm -rf ~/Library/Caches/CocoaPods
rm -rf ~/Library/Caches/org.swift.swiftpm
xcodebuild -resolvePackageDependencies \
-project App.xcodeproj \
-scheme App \
-clonedSourcePackagesDirPath "$PWD/.spm-cache"
- name: Build
run: |
xcodebuild -project App.xcodeproj \
-scheme App \
-destination 'generic/platform=iOS' \
-clonedSourcePackagesDirPath "$PWD/.spm-cache" \
-onlyUsePackageVersionsFromResolvedFile \
build
Cache .spm-cache with the hash of Package.resolved as the key, and drop any cache keyed on Podfile.lock. -onlyUsePackageVersionsFromResolvedFile makes CI fail loudly on a resolution mismatch instead of quietly pulling something newer.
Step 6: Verify, then keep the exit open
Run a clean build from an empty derived data directory, then exercise the paths that dependency managers actually break: login, a database read, push registration, a crash upload, and one analytics event. Those five cover most of what a migration silently damages.
Rollback stays cheap for as long as you keep the branch: git revert the migration commit, restore archive/cocoapods/, and pod install. Once you delete that archive, rollback becomes a rewrite.
Three pitfalls that cost real time
Private pods. A private pod becomes a Swift package hosted in a private Git repository. Xcode will prompt for credentials locally; CI needs an SSH key or a token wired into git. If the private pod ships as a prebuilt binary, it cannot become a normal package — it becomes a .binaryTarget, and the artifact must be an xcframework.
Binary dependencies. For a local artifact:
.binaryTarget(name: "VendorSDK", path: "Vendor/VendorSDK.xcframework")
For a remote one you need both url and checksum, and the checksum must be recomputed every time the artifact changes. Plain .framework files and static .a libraries are not accepted; they have to be repackaged as an xcframework first.
Resource bundles. CocoaPods resource_bundles become SPM resources on the target:
.target(
name: "DesignSystem",
resources: [.process("Resources")]
)
Use .process for images and asset catalogs so they get optimized, .copy for directories whose structure must be preserved. Access them with Bundle.module — and remember that Bundle.module only exists inside the package that declares the resources. If the app needs to reach them, expose a small accessor from the package instead of referring to Bundle.module from app code, which will not compile.
One judgment
This is a registry freeze, not a shutdown: nothing you shipped breaks on December 2. But a twenty-dependency migration is a multi-week job with a binary-artifact worst case, and the November test run is when other people's automation finds its bugs. If CocoaPods is still in your Podfile, put the branch on the calendar this month.
---
More iOS migration write-ups and project notes: hhfa008.github.io
Firebase というか、iOS の Firestore と Android の App Distribution か。Firebase 関連パッケージ全部最新化する必要はなさそう。
firebase-ios-sdk#16930
121 views
firebase-ios-sdk#15764 踏んだみたい
33 views
Mentions
Firebase iOS Push Notifications: APNs Key and SDK Setup Firebase Cloud Messaging (FCM) can deliver push notifications to iPhones, but it doesn't talk to iOS devices directly — it goes through Apple Push Notification service (APNs). Most “Firebase iOS push isn't working” problems come from the Apple side of that handshake. This guide sets up both sides in order. How Firebase push works on iOS Your app registers with APNs and receives an APNs device token. The Firebase iOS SDK maps that token to an FCM registration token, which is the one your server uses. When you send a message, FCM authenticates with APNs using the key you uploaded, and APNs delivers it to the device. If Firebase can't authenticate with APNs, nothing arrives — even though everything looks fine in your app. Prerequisites - A paid Apple Developer Program membership — push needs one. - A Firebase project with your iOS app added, and its GoogleService-Info.plist. - Xcode and a physical iPhone or iPad. The Simulator isn't a reliable test for remote push. 1. Create an APNs authentication key 1. In the Apple Developer portal, open Certificates, Identifiers & Profiles → Keys and create a new key. 2. Enable Apple Push Notifications service (APNs) for the key, then register it. 3. Download the .p8 file and note its Key ID. Apple lets you download it only once, so store it somewhere safe. 4. Note your Team ID, shown in your developer account's membership details. An authentication key doesn't expire like a push certificate, and one key works for all your apps — which is why it's the recommended option over the older .p12 certificates. 2. Upload the key to Firebase In the Firebase console, open Project settings → Cloud Messaging. Under your Apple app, upload the .p8 key and enter the Key ID and Team ID. Firebase uses this key to authenticate with APNs on your behalf. 3. Add the Firebase iOS SDK In Xcode, choose File → Add Package Dependencies and add https://github.com/firebase/firebase-ios-sdk. Select the FirebaseMessaging product. Then drag GoogleService-Info.plist into your app target. 4. Enable capabilities Select your app target, open Signing & Capabilities, and add Push Notifications. Add Background Modes and check Remote notifications if you send silent or data-only pushes. 5. Request permission and get the FCM token Configure Firebase at launch, ask the user for permission, register for remote notifications, and listen for the FCM token: import UIKit import FirebaseCore import FirebaseMessaging import UserNotifications @main class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterDelegate, MessagingDelegate { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { FirebaseApp.configure() Messaging.messaging().delegate = self UNUserNotificationCenter.current().delegate = self UNUserNotificationCenter.current().requestAuthorization( options: [.alert, .badge, .sound] ) { granted, error in // Respect the user's choice; you can ask again later from settings } application.registerForRemoteNotifications() return true } // Called with a new or refreshed FCM registration token func messaging(_ messaging: Messaging, didReceiveRegistrationToken fcmToken: String?) { guard let fcmToken else { return } // Send this token to your push server print("FCM token: \(fcmToken)") } // Show notifications while the app is in the foreground func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification) async -> UNNotificationPresentationOptions { [.banner, .sound, .badge] } } By default the Firebase SDK uses method swizzling to pass the APNs token to FCM automatically. If you set FirebaseAppDelegateProxyEnabled to NO in your Info.plist, you must pass the APNs token yourself by setting Messaging.messaging().apnsToken in didRegisterForRemoteNotificationsWithDeviceToken. 6. Send a test notification Run the app on a physical device, accept the permission prompt, and copy the FCM token from the console. In the Firebase console, open Messaging, create a notification, and use Send test message with that token. Test with the app in the foreground, in the background, and fully closed. Why Firebase iOS push isn't working - No APNs key in Firebase: the token is generated, but nothing is delivered. Check Project settings → Cloud Messaging. - Wrong Team ID or Key ID: APNs rejects Firebase's requests. Re-enter both from your Apple developer account. - Bundle ID mismatch: the iOS app in Firebase must use exactly the same bundle ID as your Xcode target. - Push Notifications capability missing: the app never receives an APNs token, so no FCM token works. - Swizzling disabled without passing the APNs token: FCM has no APNs token to map. - Permission denied: the user declined the prompt; notifications are delivered silently or not shown. Using React Native or Flutter? The Apple-side steps — APNs key, Firebase upload, and capabilities — are the same. The notifySphere SDKs handle the token and permission code for you: see the React Native guide and the Flutter guide. notifySphere runs on Firebase Cloud Messaging, so once your Firebase project can reach APNs, uploading its service account lets you send segmented iOS, Android, and web campaigns from one dashboard. --- Originally published on the notifySphere blog. notifySphere is push notifications, WhatsApp Business API, deep linking, and in-app support in one SDK — free up to 1,000 users.
Swift Package ManagerをTuist経由で導入し、CocoaPodsと併用しながら移行する 株式会社DIGITALIOで開発しているiOS版 ECナビアンケートアプリ は、依存関係の管理にCocoaPodsを使っていました。本記事では、依存関係の管理をCocoaPodsからSPM(Swift Package Manager)に移行した経緯と手法について書きます。 TL;DR 想定読者: Tuistでプロジェクトを生成していて、CocoaPodsからSPMへの移行を検討している方 依存関係の宣言をTuistの定義ファイルに集約でき、移行は「定義ファイルへの追加と Podfile からの削除」の繰り返しで済んだ 広告メディエーションのSDK本体とアダプターだけCocoaPodsに残し、SPMと併用で運用できている 前提 本記事は、TuistでXcodeプロジェクトを生成している環境を前提にしています。前回の記事では、 project.pbxproj のコンフリクトをなくすためにTuistを導入し、ビルドターゲットを Project.swift に定義して tuist generate で生成する運用に切り替えました。 zenn.dev 生成物はGit管理から除外しているため、ブランチの切り替えやファイルの追加のたびに make sync-develop でプロジェクトを再生成しています。 SPM移行のきっかけ きっかけはFirebaseから届いたメールでした。Firebase SDKは2026年10月にCocoaPodsへの新バージョン公開を停止し、以降はpodspecからのインストールが公式サポートの対象外となり、SPMまたは手動インストールへの移行が案内される、という内容です。手順は公式の移行ガイドにまとまっています。 firebase.google.com これはFirebase固有の話ではありません。CocoaPods Trunk自体も、2026年12月2日にread-onlyモードへ移行することが以前から告知されており、同じ流れにあります1。 blog.cocoapods.org CocoaPods経由でFirebaseを使い続けても公開済みのバージョンは動きますが、新機能やバグ修正は受け取れなくなります2。この告知を機に、依存関係管理をCocoaPodsからSPMへ移行することにしました。 依存関係を種類ごとに分けて移行 まず、Tuist経由でSPMを利用するために、依存関係を記述する Tuist/Package.swift を作成しました。 // swift-tools-version: 5.9 import PackageDescription #if TUIST import ProjectDescription let packageSettings = PackageSettings( productTypes: [:] ) #endif let package = Package( name: "ECNaviEnqueteApp", dependencies: [ // ここに追記していく ] ) 次に、 Podfile に書かれていた依存関係を、ネットワーク、認証、UI、その他の依存関係といった種類ごとに分類して順番に移行しました。種類ごとに分けたのは、1つのPRで動作確認すべき範囲を絞り、問題が起きたときに原因を特定しやすくするためです。 どのタスクも手順自体は共通で、次の3ステップです。実際にFirebaseを移行した例で見てみます。 Tuist/Package.swift に対象の依存関係を追加する // ... - .package( - url: "firebase/firebase-ios-sdk", - exact: "12.14.0" - ), .package( url: "kaishin/Gifu", exact: "3.4.0" ), Project.swift で .external(name:) として依存関係を宣言する // ... - .external(name: "FirebaseAnalytics"), - .external(name: "FirebaseMessaging"), - .external(name: "FirebaseRemoteConfig"), - .external(name: "FirebaseInAppMessaging-Beta"), - .external(name: "FirebaseCrashlytics"), .external(name: "Gifu"), CocoaPodsではsubspec( Firebase/InAppMessaging など)で選んでいた機能が、SPMでは1つのパッケージが公開する複数のプロダクトになります。 .external(name:) にはSPM側のプロダクト名を指定するため、Pod側の名前と異なるものもあります。 Podfile から該当のPodを削除する - # Firebase - pod 'Firebase/Core', '>10.22.0' - pod 'Firebase/Analytics' - pod 'Firebase/Messaging' - pod 'Firebase/RemoteConfig' - pod 'Firebase/InAppMessaging' - pod 'Firebase/Crashlytics' - ... 依存関係の解決はTuistが担うようになるため、 Makefile に tuist install を追加しました。依存関係の解決はプロジェクト生成時に済んでいる必要があるため、 tuist generate の前に置いています。 sync-develop: cp ECNaviEnqueteApp/Resource/GoogleService-Info-Develop.plist ECNaviEnqueteApp/Resource/GoogleService-Info.plist - tuist install tuist generate --no-open bundle exec pod install 変更を反映するには make sync-develop を実行します。次の順にコマンドが動き、SPMとCocoaPodsのロックファイルも更新されます。 移行でつまずいた点と残った課題 SPMへの移行では、単に依存関係の宣言先を変えるだけでなく、依存関係の利用方法によって個別対応が必要になったものもありました。 スクリプトのパスが変わった 移行した依存関係の1つであるFirebase Crashlyticsは、ビルド後にdSYMをアップロードするビルドスクリプト(Run Script)を実行します。 このスクリプトは、CocoaPodsでは Pods/ 配下にありましたが、SPMでは取得したパッケージのディレクトリ内に移ります。Tuistはこれを Tuist/.build/checkouts/ 配下に置くため、移行のタイミングで定義ファイルのスクリプトが参照するパスも変更しました。 - "${PODS_ROOT}/FirebaseCrashlytics/run" - "${SRCROOT}/Tuist/.build/checkouts/firebase-ios-sdk/Crashlytics/run" コードの追加対応を伴ったもの 依存関係によっては、コードの追加が必要になった箇所もありました。例えば、ネットワークレイヤーを抽象化するMoyaをCocoaPodsからSPMに移行してビルドし直すと、次のようなエラーが出ました。 FooBarClass.swift:xx:yy Cannot find 'UIDevice' in scope 原因は、CocoaPodsが各Podに生成するumbrella header( Moya-umbrella.h )とmodulemapでした。新規のプロジェクトで Podfile に use_frameworks! を指定してMoyaを導入すると、次のようなファイルが生成されます(抜粋)。 // Moya-umbrella.h #ifdef OBJC #import <UIKit/UIKit.h> #else // ... // Moya.modulemap framework module Moya { umbrella header "Moya-umbrella.h" export * module * { export * } } umbrella headerがUIKitを、UIKitがFoundationを読み込み、modulemapの export * がそれらを再エクスポートします。そのため、 import Moya と書くだけでFoundationとUIKit双方のシンボルが使えていました。 import Moya │ ├─ CocoaPods時代: FoundationもUIKitもついてきた ٩(ˊᗜˋ*)و │ └─ SPM: Moyaしか来ない (;゚Д゚) 「UIDeviceって誰…?」 SPM経由ではこの経路が失われます。その結果、実際にはFoundationやUIKitのAPIを直接使っているのに、Moyaのimportだけに依存していた数ファイルがコンパイルエラーになりました。 対象ファイルに import Foundation / import UIKit を追加して解決しました。暗黙の再エクスポートは他のPodでも起きうるため、移行対象を分けておくと、コンパイルエラーの原因を直前に移したPodに絞り込めます。 一部の依存関係はSPM対応待ち すべての依存関係をSPMへ移行できたわけではありません。広告配信のために利用している広告メディエーションでは、SDK本体こそSPM対応が完了していました。しかし、私たちが利用している複数の広告ネットワークに対応するアダプターは、CocoaPods版のSDK本体に依存する構成でした。 アダプターのPodはSDK本体のPodを依存関係として引き込むため、SDK本体だけSPMへ移すと、同じSDKがCocoaPods版とSPM版の2系統で宣言された状態になります。検証したところ、ビルドは通りました。 ただし版が異なる場合、どちらが採用されるかを示す警告は出ず、手元の検証ではSPM版が採用されました3。意図しない版が混ざる状態を避けるため、アダプターのSPM対応が済むまでは、SDK本体もCocoaPodsに残すこととしました。 開発ツールはmiseで管理 本アプリでは、Swiftの静的解析ツールであるSwiftLintと、アプリで使用しているオープンソースライブラリのライセンス一覧を自動生成するLicensePlistを使用しています。 これらは、アプリ本体にリンクされるわけではない開発ツールです。CocoaPods管理から切り出すにあたって、前回Tuistのバージョン固定用に導入したmiseへ寄せ、ツールのバージョン管理を一元化しました。 [tools] - license-plist = "3.27.9" - swiftlint = "0.63.3" tuist = "4.195.9" これらを呼ぶスクリプトは、 Pods/ 配下のパスからmiseのshimを指すように変更しました。 - "${PODS_ROOT}/SwiftLint/swiftlint" - "${MISE_DATA_DIR:-${HOME}/.local/share/mise}/shims/swiftlint" なお、LicensePlistは、SPM管理のパッケージを検出するための設定が必要でした。LicensePlistは、SPMのパッケージを Package.resolved から探します。しかし本アプリでは、Tuistが Tuist/Package.resolved を管理しており、LicensePlistの既定の探索先には存在せず、アプリ内のライセンス一覧に含まれませんでした4。 そこで、 license_plist.yml の packagePaths に Tuist/Package.swift を指定すると、同じディレクトリの Package.resolved が参照されるようになりました。 - options: - packagePaths: - - Tuist/Package.swift 移行後の状況 一連の移行の結果、依存関係の状態は次のようになりました。 状態 対象 SPMへ移行済み ネットワーク、認証、UI、その他の依存関係 miseへ移行済み SwiftLint、LicensePlist CocoaPodsのまま維持 広告メディエーションSDK本体とアダプター Podfile に書かれていたSDK本体とアダプター以外の21件のPod宣言は、すべてなくなりました。残りの依存関係は、提供元のリリースノートでSPM対応の有無を確認しながら追従していきます。 まとめ Tuistの .external(name:) で依存関係を宣言できるため、移行作業の多くは定義ファイルへの追加と Podfile からの削除という小さな差分の繰り返しになりました。種類ごとに分けて、独立してレビュー・マージできる単位で進めたことで、各PRも小さく保てました。同様の移行を控えている場合は、以下の手順で実施するとスムーズに移行が進められると思います。 前回の記事 を参考にXcodeプロジェクトの管理方式を決める。 依存関係ごとに、SPMへの対応状況を調べる。 種類ごとに分け、動作確認しやすい粒度で順番に移行する。 CocoaPodsに残る依存関係は、12月2日以降は新バージョンがTrunkに公開されなくなります。提供元のSPM対応状況を確認し、未対応のものはアダプターごと移行できる時期を見極める必要があります。 脚注 read-only化後も既存のPodは取得できるが、Trunkに新しいPodやバージョンは公開できなくなる。独自のspecリポジトリで配布されているPodは影響を受けない。 ↩︎ Firebase公式の移行ガイドによれば、CocoaPodsで最後に公開されるメジャーバージョンは12とされている。 ↩︎ CocoaPodsとSPMのバージョンを入れ替えて2通りで検証した。いずれも、ビルドしたバイナリに含まれていたのはSPM版で、新旧は関係なかった。どのパターンでも同じかまでは検証できていない。 ↩︎ 既定の探索先は、プロジェクトルートの Package.swift と、 *.xcodeproj および *.xcworkspace 配下の xcshareddata/swiftpm/Package.resolved である。詳細は LicensePlistのREADME を参照。 ↩︎
firebase-ios-sdk#16972 Firebase v13からSceneDelegateを考慮するようになってFIAMのactionURLがhttpsだとどんなURLもUniversalLinkとして流れてくるのが困るよーというIssue
Google's postmortem (October 2, 2026): firebase.blog The GitHub issue that tracked the outage while the status pages stayed green: firebase-ios-sdk#16728 The Facebook SDK crash of May 2020: facebook-ios-sdk#1373
11/ Google's postmortem (October 2, 2026): firebase.blog The GitHub issue that tracked the outage while the status pages stayed green: firebase-ios-sdk#16728
CocoaPods Is Going Read-Only: Migrating a 20+ Dependency iOS App to Swift Package Manager Your Podfile still installs. That is exactly why this is easy to postpone — and why a project with twenty-plus dependencies should start now rather than in December. First, what actually changes CocoaPods trunk is moving to a read-only state. In the official words: > "December 2nd 2026 — I will switch trunk to not accept new Podspecs permanently." And from Firebase: > "Firebase 12 is the final major version published to CocoaPods, and Firebase will stop publishing new versions to CocoaPods in October 2026." Read-only is not shut down. Existing pod versions stay downloadable, the Specs repo and CDN keep serving, and pod install keeps resolving what it resolves today. CocoaPods is explicit about this: "Making the switch will not break builds for people using CocoaPods in 2026 onwards." Firebase says the same about its own packages: existing versions "in CocoaPods will remain installable and functional as long as CocoaPods ecosystem support continues." What you lose after December is the ability to receive new versions through trunk. For an app that ships quarterly, that is a slow, undramatic problem — the worst kind, because nothing pages you at 3 a.m. Timeline | When | What happens | |---|---| | 2024-11-30 | CocoaPods announces the plan: "In two years we plan to turn CocoaPods trunk to be read-only." | | Mid–late 2025 | First email to everyone who contributed a Podspec | | Sep–Oct 2026 | Second email, roughly one month before the test run | | Oct 2026 | Firebase stops publishing to CocoaPods; Firebase 12 is the last major version | | Nov 1–7, 2026 | Test run of read-only, "giving automation a chance to break early" | | Dec 2, 2026 | Trunk permanently stops accepting new Podspecs | | After | Existing versions remain installable; Specs repo and CDN keep serving | CocoaPods adds a caveat worth quoting: "These dates are not set in stone." Treat the December date as a planning deadline, not a hard API cutoff. Flutter has already moved in the same direction — starting with Flutter 3.44, Swift Package Manager replaces CocoaPods as the default for iOS and macOS apps — so the direction of travel is not in question. Step 1: Inventory and freeze Do this on a branch, and commit the current working state before you touch anything. cd "$(git rev-parse --show-toplevel)" git checkout -b spm-migration grep -nE "^\s*pod " Podfile | tee /tmp/pods.txt wc -l < /tmp/pods.txt git add Podfile Podfile.lock git commit -m "chore: snapshot CocoaPods state before SPM migration" Then go through /tmp/pods.txt line by line and mark each dependency: has a Swift package, has a binary xcframework, or has neither. The third bucket decides your schedule. Twenty pods where eighteen migrate cleanly is a week; three pods with no package support can eat a month. One thing people miss: CocoaPods subspecs do not map one-to-one onto Swift packages. A pod like Vendor/Core and Vendor/UI may become two products of a single package, or one product with everything included. Read the vendor's own SPM instructions. Step 2: Deintegrate Quit Xcode first. pod deintegrate rewrites the project file, and an open Xcode window will happily overwrite those changes when it saves. pod deintegrate pod clean mkdir -p archive/cocoapods git mv Podfile archive/cocoapods/Podfile git mv Podfile.lock archive/cocoapods/Podfile.lock rm -rf Pods *.xcworkspace git status --short Archive, do not delete. The Podfile.lock is your rollback artifact for the rest of the migration. Step 3: Add the packages For an app target, the path of least resistance is Xcode: open the .xcodeproj, choose File → Add Package Dependencies, paste the repository URL, pick the products, and confirm they are added to the target's "Frameworks, Libraries, and Embedded Content." If your app is split into local Swift packages, declare the dependency in Package.swift instead: // swift-tools-version: 6.0 import PackageDescription let package = Package( name: "AppCore", platforms: [.iOS(.v17)], dependencies: [ .package( url: "https://github.com/firebase/firebase-ios-sdk", from: "12.0.0" ) ], targets: [ .target( name: "AppCore", dependencies: [ .product(name: "FirebaseAuth", package: "firebase-ios-sdk"), .product(name: "FirebaseFirestore", package: "firebase-ios-sdk") ] ) ] ) Commit Package.resolved alongside Package.swift. It is the SPM equivalent of Podfile.lock, and without it your CI and your laptop can resolve different revisions of the same branch-based dependency. Step 4: Clean the leftovers CocoaPods leaves more behind than the Pods/ directory. Check for: grep -n "PODS_ROOT\|Pods/" App.xcodeproj/project.pbxproj You are looking for build settings pointing at ${PODS_ROOT}, and build phases named [CP] Check Pods Manifest.lock, [CP] Embed Pods Frameworks, and [CP] Copy Pods Resources. Remove them. Keep the Crashlytics dSYM upload run script, but note that Firebase documents a different script path for SPM checkouts than for CocoaPods installs. Copy the current version from the Crashlytics setup page rather than editing the old path by hand. A stale path here fails silently: uploads just stop, and you find out weeks later when a crash report has no symbols. Step 5: CI The failure mode in CI is almost always cache, not code. Old caches still contain a Pods/ directory and a stale workspace that no longer exists in the repo. - name: Resolve packages run: | rm -rf ~/Library/Caches/CocoaPods rm -rf ~/Library/Caches/org.swift.swiftpm xcodebuild -resolvePackageDependencies \ -project App.xcodeproj \ -scheme App \ -clonedSourcePackagesDirPath "$PWD/.spm-cache" - name: Build run: | xcodebuild -project App.xcodeproj \ -scheme App \ -destination 'generic/platform=iOS' \ -clonedSourcePackagesDirPath "$PWD/.spm-cache" \ -onlyUsePackageVersionsFromResolvedFile \ build Cache .spm-cache with the hash of Package.resolved as the key, and drop any cache keyed on Podfile.lock. -onlyUsePackageVersionsFromResolvedFile makes CI fail loudly on a resolution mismatch instead of quietly pulling something newer. Step 6: Verify, then keep the exit open Run a clean build from an empty derived data directory, then exercise the paths that dependency managers actually break: login, a database read, push registration, a crash upload, and one analytics event. Those five cover most of what a migration silently damages. Rollback stays cheap for as long as you keep the branch: git revert the migration commit, restore archive/cocoapods/, and pod install. Once you delete that archive, rollback becomes a rewrite. Three pitfalls that cost real time Private pods. A private pod becomes a Swift package hosted in a private Git repository. Xcode will prompt for credentials locally; CI needs an SSH key or a token wired into git. If the private pod ships as a prebuilt binary, it cannot become a normal package — it becomes a .binaryTarget, and the artifact must be an xcframework. Binary dependencies. For a local artifact: .binaryTarget(name: "VendorSDK", path: "Vendor/VendorSDK.xcframework") For a remote one you need both url and checksum, and the checksum must be recomputed every time the artifact changes. Plain .framework files and static .a libraries are not accepted; they have to be repackaged as an xcframework first. Resource bundles. CocoaPods resource_bundles become SPM resources on the target: .target( name: "DesignSystem", resources: [.process("Resources")] ) Use .process for images and asset catalogs so they get optimized, .copy for directories whose structure must be preserved. Access them with Bundle.module — and remember that Bundle.module only exists inside the package that declares the resources. If the app needs to reach them, expose a small accessor from the package instead of referring to Bundle.module from app code, which will not compile. One judgment This is a registry freeze, not a shutdown: nothing you shipped breaks on December 2. But a twenty-dependency migration is a multi-week job with a binary-artifact worst case, and the November test run is when other people's automation finds its bugs. If CocoaPods is still in your Podfile, put the branch on the calendar this month. --- More iOS migration write-ups and project notes: hhfa008.github.io
Firebase というか、iOS の Firestore と Android の App Distribution か。Firebase 関連パッケージ全部最新化する必要はなさそう。 firebase-ios-sdk#16930
firebase-ios-sdk#15764 踏んだみたい