素振り用iOSプロジェクト fcm編

前回

素振り用iOSプロジェクト fastlane match編 - kz-kazuki’s blog

今回のゴール

firebase cloud messaging(以下fcm)を使ってpush通知を送ることができる

firebaseのドキュメント読んでポチポチすれば終わるが...

firebase

新しくプロジェクトを作成する

f:id:assaulter:20200808164038p:plain

iOSボタンを押すと進むので指示通りに作業

f:id:assaulter:20200808164133p:plain

現時点ではまだPod推奨なので、*.xcworkspace から起動することになる

fastlaneでapns用証明書を作成する

fastlane/Fastfileにコマンドを追加 (bundle_idをAppFileに記述してるので不要

...
    desc "create develop apns cert."
    lane :create_apns_develop do
        pem(
            force: true,
            development: true,
            save_private_key: true
        )
    end

コマンドを実行する

...
[16:51:12]: Creating a new push certificate for app 'org.assaulter.base'.
Private key, p12 certificate, PEMが保存されている旨が表示
...

.p12をfirebaseにアップロード

(p8がfastlaneで作れるか調べてない。できるならそっちのほうが楽

firebase console > プロジェクトを設定 > クラウドメッセージング で、開発用APNs証明書としてアップロードする

push notificationが使えるようにする

xcode > Signing & Capabilities で Push Notificationss を追加する

以下のコマンドを追加し実行

    desc "renew develop provisioning profile."
    lane :force_develop do
        match(type: "development", force: true)
    end
$ bundle exec fastlane force_develop

dev centerでidentifierが更新されていることを確認する

push許諾とfcm連携

一旦AppDelegateに実装を追加する

import UIKit
import Firebase
import FirebaseMessaging

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
    
    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        // Override point for customization after application launch.
        FirebaseApp.configure()
        Messaging.messaging().delegate = self
        UNUserNotificationCenter.current().delegate = self
        
        registerRemoteNotification()
        
        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.
    }
}

// MARK: - private

private extension AppDelegate {
    
    /// テスト用なのでcompletionは特に何もしない
    func registerRemoteNotification() {
        let authOptions: UNAuthorizationOptions = [.alert, .badge, .sound]
        UNUserNotificationCenter.current().requestAuthorization(
            options: authOptions, completionHandler: { granted, error in
                if granted {
                    DispatchQueue.main.async {
                        UIApplication.shared.registerForRemoteNotifications()
                    }
                }
        })
    }
}

// MARK: - UNUserNotificationCenterDelegate

extension AppDelegate: UNUserNotificationCenterDelegate {
    
    func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
        let deviceTokenString: String = deviceToken.map { String(format: "%.2hhx", $0) }.joined()
        debugPrint("deviceTokenString \(deviceTokenString)")
    }
    
    func application(_: UIApplication, didFailToRegisterForRemoteNotificationsWithError _: Error) {
        debugPrint("リモート通知の設定は拒否されました")
    }
    
    func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {

        let userInfo = notification.request.content.userInfo
        debugPrint("userInfo: \(userInfo)")

        completionHandler([.alert, .sound])
    }
}

// MARK: - MessagingDelegate

extension AppDelegate: MessagingDelegate {
    
    func messaging(_ messaging: Messaging, didReceiveRegistrationToken fcmToken: String) {
        debugPrint("fcmToken: \(fcmToken)")
        // TODO: If necessary send token to application server.
        // Note: This callback is fired at each app startup and whenever a new token is generated.
    }
}

実機でアプリを起動し、許諾を承認するとfcmTokenがログに出る

fcm http api v1を使って動作確認

firebase console > プロジェクトの設定 > サービスアカウント > Firebase Admin SDK > 「新しい秘密鍵を生成」を押す

落ちてきたjsonファイルは保管しておく

認証とリクエストを行うrubyスクリプト

雑に

# frozen_string_literal: true

source "https://rubygems.org"

gem "google-api-client"
gem "faraday"
gem "googleauth"
require 'bundler'
Bundler.require
require 'json'

# Config
file_path = "service account key file path"
project_id = "Your firebase project id"
token = "fcmToken from app."

# get access token
authorizer = Google::Auth::ServiceAccountCredentials.make_creds(
    json_key_io: File.open(file_path),
    scope: 'https://www.googleapis.com/auth/firebase.messaging'
)
access_token = authorizer.fetch_access_token!
access_key = "#{access_token['token_type']} #{access_token['access_token']}"

# request
uri = URI.parse("https://fcm.googleapis.com/v1/projects/#{project_id}/messages:send")
https = Net::HTTP.new(uri.host, uri.port)
https.use_ssl = true
req = Net::HTTP::Post.new(uri)
req['Content-Type'] = 'application/json'
req['Authorization'] = access_key
req.body = {
    message: {
        token: token,
        notification: {
            title: 'test push notification',
            body: 'notification message from fcm.'
        }
    }
}.to_json

res = https.request(req)
p res

configの部分を埋めて実行するとpushが飛ぶのを確認できる