[進階]使用Firebase function服務建立自動推播系統


Firebase Cloud Messaging 提供跨平台推播功能服務,不過今天我想更進一步整合Firebase其他服務來達成一個自動推播的系統。

假設手機A想要發送推播給B,如果企業有自己的DB和APN Server,自然可以透過呼叫後端API來達成想要的功能。但是畢竟自己架設後台也是一個負擔,所以如果可以透過使用Firebase的服務可以省下不少* (Firebase 並不是完全免費的,要看使用量)

工作的流程大概如圖所示:

  1. 手機A將資料寫入Database。
  2. 透過Functions部署的Node.js監控Database變化,如果有資料異動,則依造寫入的資料讀取token,將token丟給Messaging去發送推播。
  3. 推播完成,刪除暫存資料。
所以可以發現,最重要的部分是Node.js的部署,由Node.js寫的function去監控和觸發事件。

萬事起頭難,讓我們一步步來建立這樣的環境吧!

前置作業:

  • XCode & CocoaPods 
  • Apple 開發者帳號
  • iOS App使用 Swift

環境建立Building environment - Firebase & Node.js

*如果尚未安裝Node JS和NPM,請先安裝後再繼續。
  1. 建立一個新的 Firebase project 
  2. 登記Web 和 iOS app 
  3. 在Local 建立一個folder 作為要部署之用,建立完成之後,在該資料夾下進行Node 初始化:
    npm init 
  4. 接下來安裝Firebase CLI:
    
    npm install -g firebase-tools
    
    
  5. 接下來登入並且初始化Project:
    
    firebase login 
    firebase init functions
    
    
  6. 結束後應該可以看到functions的資料夾已經建立好了。
  7. 測試看看是否可以使用。打開index.js 把 exports.helloWorld 這段unmark。存檔後發布:
    
    firebase deploy --only functions
    
  8. 打開瀏覽器並輸入Function URL你可以看到“Hello from Firebase!” 

環境建立 Xcode & Firebase messaging

在這篇我們要建立Client - App部分,也就是設定App可以接收Notification。我們需要Firebase messaging SDK來取得token,並將token寫到Firebase database。詳細的步驟可以參考這裏

步驟:
  1. 建立新的Project (或利用舊有的)。並且開啟Push Notification (App > Capabilities) 。並且記得要把Firebase的GoogleServices-Info.plist 加到專案內。
  2. 用Cocoapods 安裝Firebase messaging. 除了Firebase SDK ,我另外加了Toast-Swift作為在App開啟的狀態下顯示推播用。
      target 'testApp' do# Comment the next line if you don't want to use dynamic frameworks
      use_frameworks!
      pod 'Firebase/Messaging'
      pod 'Firebase/Auth'
      pod 'Firebase/Database'
      pod 'Toast-Swift' //給App at Foreground
      # Pods for testApp

    end
  3. 安裝pods後開啟專案(.xcworkspace) 
  4. 開啟AppDelegate.swift ,加入Notification 需要的methods
    //
    // AppDelegate.swift
    // CryptoCurrencyPayment
    //
    // Created by Nick on 4/16/20.
    // Copyright © 2020 Nick. All rights reserved.
    //
    import UIKit
    import Firebase
    @UIApplicationMain
    class AppDelegate: UIResponder, UIApplicationDelegate {
    var window: UIWindow?
    let gcmMessageIDKey = "gcm.message_id"
    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
    // Override point for customization after application launch.
    self.window = UIWindow(frame: UIScreen.main.bounds)
    self.window?.makeKeyAndVisible()
    FirebaseApp.configure()
    // [START set_messaging_delegate]
    Messaging.messaging().delegate = self
    // [END set_messaging_delegate]
    // [START register_for_notifications]
    //Register for remote notifications
    if #available(iOS 10.0, *) {
    // For iOS 10 display notification (sent via APNS)
    UNUserNotificationCenter.current().delegate = self
    let authOptions: UNAuthorizationOptions = [.alert, .badge, .sound]
    UNUserNotificationCenter.current().requestAuthorization(
    options: authOptions,
    completionHandler: {_, _ in })
    } else {
    let settings: UIUserNotificationSettings =
    UIUserNotificationSettings(types: [.alert, .badge, .sound], categories: nil)
    application.registerUserNotificationSettings(settings)
    }
    application.registerForRemoteNotifications()
    // [END register_for_notifications]
    return true
    }
    // MARK: UISceneSession Lifecycle
    func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
    // Called when a new scene session is being created.
    // Use this method to select a configuration to create the new scene with.
    return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
    }
    func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set<UISceneSession>) {
    // Called when the user discards a scene session.
    // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions.
    // Use this method to release any resources that were specific to the discarded scenes, as they will not return.
    }
    // [START receive_message]
    func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable: Any]) {
    // If you are receiving a notification message while your app is in the background,
    // this callback will not be fired till the user taps on the notification launching the application.
    // TODO: Handle data of notification
    // With swizzling disabled you must let Messaging know about the message, for Analytics
    Messaging.messaging().appDidReceiveMessage(userInfo)
    // Print message ID.
    if let messageID = userInfo[gcmMessageIDKey] {
    print("Message ID: \(messageID)")
    }
    NotificationCenter.default.post(name: .didReceiveMoney, object: nil, userInfo: userInfo)
    // Print full message.
    print(userInfo)
    }
    func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable: Any],
    fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
    // If you are receiving a notification message while your app is in the background,
    // this callback will not be fired till the user taps on the notification launching the application.
    // TODO: Handle data of notification
    // With swizzling disabled you must let Messaging know about the message, for Analytics
    Messaging.messaging().appDidReceiveMessage(userInfo)
    // Print message ID.
    if let messageID = userInfo[gcmMessageIDKey] {
    print("Message ID: \(messageID)")
    }
    NotificationCenter.default.post(name: .didReceiveMoney, object: nil, userInfo: userInfo)
    // Print full message.
    print(userInfo)
    completionHandler(UIBackgroundFetchResult.newData)
    }
    // [END receive_message]
    func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) {
    print("Unable to register for remote notifications: \(error.localizedDescription)")
    }
    // This function is added here only for debugging purposes, and can be removed if swizzling is enabled.
    // If swizzling is disabled then this function must be implemented so that the APNs token can be paired to
    // the FCM registration token.
    func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
    print("APNs token retrieved: \(deviceToken)")
    // With swizzling disabled you must set the APNs token here.
    // Messaging.messaging().apnsToken = deviceToken
    }
    }
    // [START ios_10_message_handling]
    @available(iOS 10, *)
    extension AppDelegate : UNUserNotificationCenterDelegate {
    // Receive displayed notifications for iOS 10 devices.
    func userNotificationCenter(_ center: UNUserNotificationCenter,
    willPresent notification: UNNotification,
    withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
    let userInfo = notification.request.content.userInfo
    // With swizzling disabled you must let Messaging know about the message, for Analytics
    // Messaging.messaging().appDidReceiveMessage(userInfo)
    // Print message ID.
    if let messageID = userInfo[gcmMessageIDKey] {
    print("Message ID: \(messageID)")
    }
    NotificationCenter.default.post(name: .didReceiveMoney, object: nil, userInfo: userInfo)
    // Print full message.
    print(userInfo)
    // Change this to your preferred presentation option
    completionHandler([])
    }
    func userNotificationCenter(_ center: UNUserNotificationCenter,
    didReceive response: UNNotificationResponse,
    withCompletionHandler completionHandler: @escaping () -> Void) {
    let userInfo = response.notification.request.content.userInfo
    // Print message ID.
    if let messageID = userInfo[gcmMessageIDKey] {
    print("Message ID: \(messageID)")
    }
    NotificationCenter.default.post(name: .didReceiveMoney, object: nil, userInfo: userInfo)
    // Print full message.
    print(userInfo)
    completionHandler()
    }
    }
    // [END ios_10_message_handling]
    extension AppDelegate : MessagingDelegate {
    // [START refresh_token]
    func messaging(_ messaging: Messaging, didReceiveRegistrationToken fcmToken: String) {
    print("Firebase registration token: \(fcmToken)")
    // let dataDict:[String: String] = ["token": fcmToken]
    // NotificationCenter.default.post(name: Notification.Name("FCMToken"), object: nil, userInfo: dataDict)
    // TODO: If necessary send token to application server.
    // Note: This callback is fired at each app startup and whenever a new token is generated.
    let defaults = UserDefaults.standard
    defaults.set(fcmToken, forKey: "fcmToken")
    }
    // [END refresh_token]
    // [START ios_10_data_message]
    // Receive data messages on iOS 10+ directly from FCM (bypassing APNs) when the app is in the foreground.
    // To enable direct data messages, you can set Messaging.messaging().shouldEstablishDirectChannel to true.
    func messaging(_ messaging: Messaging, didReceive remoteMessage: MessagingRemoteMessage) {
    print("Received data message: \(remoteMessage.appData)")
    }
    // [END ios_10_data_message]
    }
  5. XCode設定算是告一段落,接下來開啟Firebase project的設定(Settings > Cloud Messaging),並且上傳APNs Authentication Key。參考如何取得APN Key
  6. 安裝App 到實機上,試著將xcode console端取得的token丟到Firebase messaging 看看能不能測試發送推播。
  7. 接下要寫一些Code,將收到token 存到database。直接下載Demo專案。App中名單列表紅色表示為自己機器,主要程式碼都會在ViewController.swift 。每次按下"Add this device to DB"按鈕,就會把push token存到Firebase database。Database的結構如下:

    <Host>
     | - msgPool
     | - userList
            | - <Token>
                     |- userName : <UserName>
    資料結構中,msgPool是用來存放推播消息,基本上都是空的,只有在推播送出資料後會有資料,然後監控的node js發現有資料後會將資料讀出並發送推播,推播發送後即刪除。userList 則是存使用者的token,每按一次"Add this device to DB"就會寫入/更新最新的名稱。
     

撰寫index.js

[未完待續]

留言