📩

デバイス間でPush通知を送り合う with SwiftUI, Firebase Cloud Messaging

2021/07/15に公開

はじめに

Firebase Cloud Messagingを使用し、デバイス to デバイスでPush通知を送る実装を書いてみました。
Push通知というとFirebase Consoleからグループへ通知を打たれることが多いかと思います。
今回はそうではなく、アプリユーザーのデバイスから他の特定ユーザーのデバイスへ向けてPush通知を送る方法をSwiftUIで試してみました。

ユースケースとしては、例えば対戦ゲームなどで対戦待ちユーザーが友達に向かって「対戦しよう」という旨のPush通知を送ったり等、リアルタイムに相手にアプリ起動を促す場面などで使えるのではと思います。

大雑把な一連の流れとしては、

  1. アプリ起動時に端末からFirebase Cloud Messaging Token(以下FCM Token)を取得
  2. 取得したFCM Tokenをデータベース等に保存(ここではFirebase Realtime Databaseを使用しています)
  3. アプリ内のユーザーが他の特定ユーザーに向かってPush通知を送るアクションを起こす(Buttonタップ等)
  4. データベースに保存してある送信先ユーザーのFCM Tokenを使って、FCMで用意されているPush通知送信APIを叩く -> Push通知が打たれる
  5. Push通知を受け取ったユーザーが通知をタップ
  6. アプリ起動後、特定画面に遷移

大雑把に書くとこのようになります。
届いたPush通知をタップ後、理想としては「通知を送ったユーザーがいる画面」と同じ画面を開きたいので、通知タップ後に特定画面に遷移するまでをやってみたいと思います。

前提

OS: iOS14.0 ~
Interface: SwiftUI
Life Cycle: SwiftUI App
ライブラリ管理ツール: Swift Package Manager
-> Firebase iOS SDK v8.0.0から正式にSPMがサポートされました🎉

  • アプリをFirebaseに登録済み
  • Firebaseライブラリをインポート済み
  • データベースはRealtime Databaseを使用

0. CloudMessagingの設定

アプリでPush通知を利用できるようにするために、APNs証明書を作成したり、作成した証明書をFirebase Consoleに登録したり、色々準備をする必要があります。
ここではすでに設定済みであることを前提にしていますが、詳細な設定方法は以下のリンクが大変参考になりました。
【Swift5】リモートプッシュ通知の実装方法
一通り設定し終わったあとのApp.swiftは以下のような感じになります。

ExampleApp.swift
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. アプリ起動時に端末からFCM Tokenを取得 -> 保存

CloudMessagingはアプリの起動時に、インスタンスの登録トークンというものを生成します。
このトークンをユーザーIDなど一意なものに紐付けておけば、ユーザーから別の特定ユーザーに向けてPush通知を送ることができます。

例えばFirebase RealtimeDatabaseだと、users -> uidの枝に以下画像のような感じで各々のユーザーのFCM Tokenを登録しておきます。
Push通知を送りたい時には、送信先ユーザーのuidからそのユーザーのTokenを取得すればOKです。

そのために、各々のユーザーがアプリ起動時に自分のFCM TokenをRealtime Databaseに保存しなければなりません。
extension AppDelegate: UNUserNotificationCenterDelegate に、以下のコードを付け足します。

ExampleApp.swift
extension AppDelegate: UNUserNotificationCenterDelegate {

   // アプリ起動時にFCM Tokenを取得、その後RDBのusersツリーにTokenを保存
   func messaging(_ messaging: Messaging, didReceiveRegistrationToken fcmToken: String?) {
      guard let fcmToken = fcmToken else { return }
      if let uid = Auth.auth().currentUser?.uid {
         setFcmToken(uid: uid, fcmToken: fcmToken)
      }
   }

   func setFcmToken(uid: String, fcmToken: String) {
      let value = ["fcmToken": fcmToken]
      let ref = Database.database().reference().child("users").child(uid)
      ref.updateChildValues(value)
   }

これで、ユーザーごとのFCM Tokenをuidに紐付けることができました。

2. Push通知送信ボタンを作る

Push通知を送るトリガーはアプリごとに違うのでなんでも良いのですが、ここでは単純に、
押したら特定の相手にPush通知が届くボタンを作ります。
一応uidで送る相手を特定するので、ログイン機能のあるアプリを想定しています。
以下のサンプルはタブメニューが2つあるTabViewです。
ログイン済みの場合そのままTabViewのtag0に指定してあるHogeViewへ、未ログインの場合はSignInViewを表示するようにしています。
FugaViewにPush通知送信ボタンを設置し、届いたPush通知を押下したユーザーはHogeViewではなくFugaViewを遷移した状態でアプリを開くようにします。

HomeView.swift
import SwiftUI

struct HomeView: View {
   @StateObject private var fbSession = FirebaseSession()
   @ObservedObject var homeViewModel = HomeViewModel()
   
   var body: some View {
      Group {
         if fbSession.session != nil {
	    TabView(selection: $homeViewModel.selection) {
	       HogeView(session: self.fbSession)
                  .tabItem {
                     VStack {
                        Image(systemName: "person.3.fill")
                        Text("Hoge")
                     }
                  }.tag(0)
               FugaView(session: self.fbSession)
                  .tabItem {
                     VStack {
                        Image(systemName: "ellipsis.bubble.fill")
                        Text("Fuga")
                     }
                  }.tag(1)
	   }
	} else {
	   SignInView()
	}
    }
    .environmentObject(session)
}
FugaView.swift
import SwiftUI

struct FugaView: View {
   @EnvironmentObject var session: FirebaseSession
   
   var body: some View {
      Spacer()
      Button(action: {
         // ここにPush通知送信メソッドを書く
      }) {
         Text("Push通知送信 📣")
      }
      Spacer()
   }
}

3. HTTPリクエスト送信処理を実装

https://fcm.googleapis.com/fcm/send のエンドポイントに、送信先のFCM Tokenを付与してPOSTリクエストを送ることで、Push通知を送ることができます。
また、ここでPush通知のタイトルと本文を決めたり、ペイロードといってPush通知に含める情報を定義したりします。
詳細は公式ドキュメントを参考にしていただければと思います。

実装するにあたり、CloudMessagingのサーバーキーが必要なので、メモしておきます。
Firebase Console -> プロジェクト概要横の歯車ボタン -> CloudMessagingタブ -> プロジェクト認証情報 にある、サーバーキーの部分です。

PushNotificationSender.swift
import Foundation

class PushNotificationSender {
   init() {}
   private let FCM_serverKey = /* Console画面で控えておいたサーバーキー */
   private let endpoint = "https://fcm.googleapis.com/fcm/send"
   
   func sendPushNotification(to token: String, userId: String, title: String, body: String, completion: @escaping () -> Void) {
      let serverKey = FCM_serverKey
      guard let url = URL(string: endpoint) else { return }
      let paramString: [String: Any] = ["to": token,
                                        "notification": ["title": title, "body": body],
                                        "data": ["userId": userId]]
      var request = URLRequest(url: url)
      request.httpMethod = "POST"
      request.httpBody = try? JSONSerialization.data(withJSONObject: paramString, options: [.prettyPrinted])
      request.setValue("application/json", forHTTPHeaderField: "Content-Type")
      request.setValue("key=\(FCM_ServerKey)", forHTTPHeaderField: "Authorization")
      let task = URLSession.shared.dataTask(with: request) { data, _, _ in
            do {
                if let jsonData = data {
                    if let jsonDataDict = try JSONSerialization.jsonObject(with: jsonData, options: JSONSerialization.ReadingOptions.allowFragments) as? [String: AnyObject] {
                        print("Received data: \(jsonDataDict)")
                    }
                }
            } catch let err as NSError {
                print(err.debugDescription)
            }
        }
	task.resume()
        completion()
   }
}

ペイロード(上のコードではparamString)の"data"キーに辞書型で登録することによって、値をPush通知に含めることができます。
上のコードでは送信者のuidを入れています。

これでAPIを叩けるようになったので、先程のFugaView内のボタンに処理を追加します。
送信先のFCM Tokenは相手のuidから取れるようになっているので、事前に取得しておきます。

FugaView.swift
import SwiftUI

struct FugaView: View {
   @EnvironmentObject var session: FirebaseSession
   let pushNotificationSender = PushNotificationSender()
   
   var body: some View {
      Spacer()
      Button(action: {
         pushNotificationSender.sendPushNotification(
	    to: /* 送信先のFCM Token */,
	    userId: /* 自分のuid */,
	    title: "Test", /* Push通知のタイトル */
	    body: "Hello" /* Push通知の本文 */) {
	    print("done")
	 }
      }) {
         Text("Push通知送信 📣")
      }
      Spacer()
   }
}

届きました!

4. Push通知がタップされた時の処理を書く

Push通知を送信することはできましたが、現状だと通知をタップしてもTabViewのtag0に指定してあるHogeViewが開かれます。
ここでは送信元がいるtag1にFugaViewを開くようにしたいので、デフォルトで開かれるHogeViewからFugaViewに遷移する処理を書きます。

具体的には、Push通知がタップされた時に呼ばれるメソッドuserNotificationCenter(didReceive response ~)内でNotificationCenterを使って通知を送り、HomeViewのViewModelでCombineを使って監視、タップイベントが通知されたらTabViewのselectionを0 -> 1に変更し、FugaViewに遷移するようにします。

AppDelegateに以下のデリゲートメソッドを追加します。
これでPush通知がタップされた時に、"didReceiveRemoteNotification"という名前で登録したNotificationCenterにイベント通知がいきます。

ExampleApp.swift
extension AppDelegate: UNUserNotificationCenterDelegate {
   //  Push通知がタップされた時の処理
    func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {
        let userInfo = response.notification.request.content.userInfo
        NotificationCenter.default.post(name: Notification.Name("didReceiveRemoteNotification"), object: nil, userInfo: userInfo)
        completionHandler()
    }
}

次にHomeViewModelを書きます。
HomeView内のTabViewは、HomeViewModelのselectionをみるようにしています。
selectionには@Publishを付けているので、値に変化があった時にその変更をHomeViewに知らせます。
また、Push通知のペイロードには送信元のuidを含めているので、それもここで取り出しています。

TabView(selection: $homeViewModel.selection)
HomeViewModel.swift
import SwiftUI
import Combine

class HomeViewModel: ObservableObject {
   @Published var selection = 0
   var cancellable: AnyCancellable?
   var userId = ""
   
   init() {
      cancellable = NotificationCenter.default
         .publisher(for: Notification.Name("didReceiveRemoteNotification"))
         .sink { notification in
	    if let userInfo = notification.userInfo, let userId = userInfo["userId"] {
               self.userId = userId as? String ?? ""
            }
            self.selection = 1
         }
   }

これでPush通知がタップされた時にHogeViewではなくFugaViewを開くようにできました!🎉
今回はTabView内での遷移でしたが、同じようやればsheetなど他の遷移方法でもできます。

ぜひ試してみてください。

GitHubで編集を提案

Discussion