2024年版 SwiftUIでFirebaseCloudMessagingを使いデバイス間でPush通知を送り合う
はじめに
Firebase Cloud Messagingを使用して、デバイス間でPush通知を送り合うコードを書いてみました
大雑把な一連の流れとしては、
1.Firebaseからサービスアカウントキーを取得
2.アプリ起動時に端末からFirebase Cloud Messaging Token(以下FCM Token)を取得
取得したFCM Tokenをデータベース等に保存(ここではFirebase Databaseを使用しています)
3.アプリ内のユーザーが他の特定ユーザーに向かってPush通知を送るアクションを起こす(Buttonタップ等)
4.データベースに保存してある送信先ユーザーのFCMTokenとサービスアカウントキーのプライベートキーからJWTキーを生成しPush通知を送信
前提
OS: macOS Sonoma 14.3.1
Xcode: Version 15.4
Interface: SwiftUI
Life Cycle: SwiftUI App
ライブラリ管理ツール: Swift Package Manager
・アプリにFirebaseを登録済み
・FirebaseライブラリとJWTKitからインポート済み
・データベースはFirebaseDatabaseを使用
0.CloudMessagingの設定
アプリでPush通知を利用できるようにするために、APNs証明書を作成したり、作成した証明書をFirebase Consoleに登録したり、色々準備をする必要があります。
ここではすでに設定済みであることを前提にしていますが、詳細な設定方法は以下のリンクが大変参考になりました。
import SwiftUI
import UserNotifications
import Firebase
import FirebaseMessaging
@main
struct ExampleApp: App {
@UIApplicationDelegateAdaptor(AppDelegate.self) var delegate
var body: some Scene {
WindowGroup {
HomeView()
}
}
}
// MARK: - AppDelegate Main
class AppDelegate: NSObject, UIApplicationDelegate, MessagingDelegate {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool {
FirebaseApp.configure()
Messaging.messaging().delegate = self
UNUserNotificationCenter.current().delegate = self
// Push通知許可のポップアップを表示
let authOptions: UNAuthorizationOptions = [.alert, .badge, .sound]
UNUserNotificationCenter.current().requestAuthorization(options: authOptions) { granted, _ in
guard granted else { return }
DispatchQueue.main.async {
application.registerForRemoteNotifications()
}
}
return true
}
}
// MARK: - AppDelegate Push Notification
extension AppDelegate: UNUserNotificationCenterDelegate {
func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable: Any], fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
if let messageID = userInfo["gcm.message_id"] {
print("MessageID: \(messageID)")
}
print(userInfo)
completionHandler(.newData)
}
// アプリがForeground時にPush通知を受信する処理
func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
completionHandler([.banner, .sound])
}
}
1.アプリ起動時に端末からFCMTokenを取得
CloudMessagingはアプリの起動時に、インスタンスの登録トークンというものを生成します。
このトークンをユーザーIDなど一意なものに紐付けておけば、ユーザーから別の特定ユーザーに向けてPush通知を送ることができます。
FirebaseStoreだと Users > uid > の枝に以下画像のような感じで各々のユーザーのFCM Tokenを登録しておきます。
そのために、各々のユーザーがアプリ起動時に自分のFCM TokenをFirebaseDatabaseに保存しなければなりません。
extension AppDelegate: UNUserNotificationCenterDelegate {
// テスト通知に必要なFCMトークンを出力する
func application(_ application: UIApplication,
didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
Messaging.messaging().apnsToken = deviceToken
Messaging.messaging().token { token, error in
if let error = error {
print("Error fetching FCM registration token: \(error)")
} else if let token = token {
print("FCM registration token: \(token)")
}
}
}
func setFCMToken(fcmToken: String) {
let db = Firestore.firestore()
db.collection("コレクション名").document("ドキュメント名").setData([
"fcmToken": fcmToken
]) { error in
if let error = error {
print("Error updating document: \(error)")
} else {
print("Document successfully updated")
}
}
}
}
2.Push通知送信ボタンを作る
アプリ毎にプッシュ通知を送るトリガーはそれぞれ違うと思うのですが、今回はボタンを押したら
プッシュ通知が届くようにします。
import SwiftUI
struct FugaView: View {
var body: some View {
Spacer()
Button(action: {
// ここにPush通知送信メソッドを書く
}) {
Text("Push通知送信 📣")
}
Spacer()
}
}
3.HTTPSリクエスト送信処理を実装
https://fcm.googleapis.com/v1/projects/プロジェクト名/messages:send のエンドポイントに、送信先のFCM Tokenを付与してPOSTリクエストを送ることで、Push通知を送ることができます。
また、ここでPush通知のタイトルと本文を決めたり、ペイロードといってPush通知に含める情報を定義したりします。
詳細は公式ドキュメントを参考にしていただければと思います。
実装するにあたり、CloudMessagingのサービスアカウントキーが必要なので、メモしておきます。
Firebase Console -> プロジェクト概要横の歯車ボタン -> サービスアカウントキータブ -> 新しい秘密鍵を生成してください。
import Foundation
import FirebaseMessaging
import FirebaseFirestore
import JWTKit
final class NotificationManager {
static let instance: NotificationManager = NotificationManager()
func sendPushNotification(fcmToken: String, Title: String, Body: String) {
guard let url = URL(string: "https://fcm.googleapis.com/v1/projects/プロジェクト名/messages:send") else {
print("Invalid URL")
return
}
let payload: [String: Any] = [
"message": [
"token": fcmToken,
"notification": [
"title": Title,
"body": Body
]
]
]
guard let jsonData = try? JSONSerialization.data(withJSONObject: payload) else {
print("Failed to create JSON data")
return
}
// トークンを生成
var token = ""
do {
token = try generateAccessToken()
print("Access token: \(token)")
} catch {
print("Failed to generate access token: \(error)")
}
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
request.httpBody = jsonData
URLSession.shared.dataTask(with: request) { data, response, error in
if let error = error {
DispatchQueue.main.async {
print("Error: \(error.localizedDescription)")
}
return
}
if let httpResponse = response as? HTTPURLResponse {
DispatchQueue.main.async {
print("Response: \(httpResponse.statusCode)")
}
}
}.resume()
}
func generateAccessToken() throws -> String {
// Replace with your actual service account private key and client email
let privateKey = """
-----BEGIN PRIVATE KEY-----\n
\n-----END PRIVATE KEY-----\n
"""
let clientEmail = "CLIENTEMAIL"
// Initialize JWTSigner for RS256 (RSA SHA-256)
let signers = JWTSigner.rs256(key: try .private(pem: privateKey))
let payload = PayloadData(
iss: clientEmail,
scope: "https://www.googleapis.com/auth/firebase.messaging",
aud: "https://oauth2.googleapis.com/token",
exp: Date().addingTimeInterval(3600), // 1 hour expiration
iat: Date()
)
// Generate JWT token
let jwt: String
do {
jwt = try signers.sign(payload)
} catch {
throw error
}
// Exchange the JWT for an access token
let url = URL(string: "https://oauth2.googleapis.com/token")!
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.addValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
let body = "grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer&assertion=\(jwt)"
request.httpBody = body.data(using: .utf8)
let semaphore = DispatchSemaphore(value: 0)
var accessToken: String?
var requestError: Error?
let task = URLSession.shared.dataTask(with: request) { data, response, error in
if let error = error {
requestError = error
print("Error requesting access token: \(error)")
} else if let data = data {
do {
if let json = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any],
let token = json["access_token"] as? String {
accessToken = token
} else {
print("Invalid response data: \(String(data: data, encoding: .utf8) ?? "nil")")
}
} catch {
requestError = error
print("Error parsing response data: \(error)")
}
}
semaphore.signal()
}
task.resume()
semaphore.wait()
if let error = requestError {
throw error
}
// Return the access token or handle error if nil
guard let token = accessToken else {
fatalError("Failed to retrieve access token")
}
return token
}
}
struct PayloadData: JWTPayload {
let iss: String
let scope: String
let aud: String
let exp: Date
let iat: Date
// Conformance to JWTPayload requires this function
func verify(using signer: JWTSigner) throws {
// Add custom verification logic if needed
}
}
ペイロード(上のコードではparamString)の"data"キーに辞書型で登録することによって、値をPush通知に含めることができます。
アクセストークンを生成するときのprivateKeyは先ほど取得したアクセストークンキー内にある
プライベートキーを貼り付けてください。
URLの"https://fcm.googleapis.com/v1/projects/プロジェクト名/messages:send"
projects以降のプロジェクト名は自分のプロジェクト名に変更してください
これでAPIを叩けるようになったので、先程のContentView内のボタンに処理を追加します。
送信先のFCM Tokenはあらかじめ取得した後に使用するようお願いします。
import SwiftUI
struct ContentView: View {
var body: some View {
Spacer()
Button(action: {
PushNotificationManager().sendPushNotification(fcmToken: String, Title: String, Body: String)
}
}) {
Text("Push通知送信 📣")
}
Spacer()
}
}
届きました!
ぜひ試してみてください。
参考記事
Discussion