はじめに
2ヶ月以上前に 「集まれSwift好き!Swift愛好会 vol43 @レバレジーズ」 にて
「iOSのログテストをFirebaseで自動化する」
という内容でLTをさせていただいたのですが、その時に話した内容の一部を抜粋したものになります。
(いまさらで大変申し訳ないが、、adventに便乗して記事化しました笑)
実装
LTでは概要を話しただけで、具体的な実装方法には触れていなかったので、今回はコードを中心に説明していこうと思います。
0. 意図
詳しいことは資料を見てもらうのが早いですが、以下の2つを実施したいと考えました。
① ログのテストをリアルアイムに確認したい
② ログのテストを端末上で完結させたい
前提として、ログの確認にはサーバーに溜まったデータをクエリーで叩く必要があり、これをフロントだけですぐに確認できる機構が欲しくてこれを検討しました。
その際、サーバーに送られた値がログとして正しいこと を担保したいので、一度サーバーに送られた値をフロント(アプリ)に返すということを考えました。
根本的にはFirebaseの管理画面(DebugView)でも確認できますが、
- いちいち管理画面開くのがめんどくさい
- 見づらい
- 複数人開発の場合は同じ管理画面に端末が複数出てしまい面倒
という理由からも、なんとかしたいという思いがありました。
1. ゴール
資料(LT)では別のゴールを提示していますが、今回はその試行錯誤した過程で生まれた部分(今回のタイトル部分)が知見になりそうだったので、そこだけを抜粋して説明していきます。
行われる流れはざっくり以下の3つ
① Firebaseのログを送る
② 送られたログをCloud Functionで検知
③ Cloud Functionで端末にPushを送る
④ アプリ上でデータハンドリングする
詳細は各章でみていきましょう。
2. 実装
今回はFirebaseの導入部分に関しては載せません。
- Push通知が受け取れる
- Firebase CLIが使える
上記が既に終わっている前提に話を進めていきます。
導入から行いたい場合は参考文献を呼んでいただくか、調べてもらえると助かります。
① Firebaseのログを送る
1. ログを送ること
ログを送ること自体はさほど難しくありません。Firebaseに準拠した送り方を実装するだけです。
var parameters: [String: Any]? =[:]
FirebaseAnalytics.Analytics.logEvent("ログ名", parameters:parameters)
ただし、Push通知を端末に返すために、どの端末にPushを送るのか端末を特定する必要があります。
Cloud FunctionsからPush通知を送るので、端末を特定するための情報もログに付与する必要があります。
2. トークンを取得する
Push通知はFirebase Cloud Messaging
の機能を使って送ります。
この機能を使用する際には、端末(アプリ)ごとにユニークに生成されるトークンを指定する必要があります。
このトークンはfcm tokenと呼ばれ、アプリで取得することができ、これをログのパラメータに付与します。
(Cloud Functionsでトークンを取り出せるようにするために)
アプリ側では
Messaging.messaging().fcmToken
こんな感じでトークンをサクッと取得できます。
(中身がオプショナルなことに注意)
3. トークンを分割する
Firebase Analytics
の制約でパラメータの値は100文字を超えるとエラーになります。
(ちなみにパラメータは25個までしか設定できないのも注意)
fcm tokenは100文字を超えてしまうため、ログに付与する際に分割して送ります。
以下、実際のトークン(赤い部分で分割される予定です)
分割の方法はなんでも良いですが、以下のようなextensionを用意すると簡単にできるでしょう。
extension String {
func split(_ length: Int) -> [String] {
guard 0 < length else { return [] }
let array = self.map { "\($0)" }
let limit = array.count
return stride(from: 0, to: limit, by: length).map {
array[$0..<min($0.advanced(by: length), limit)].joined(separator: "")
}
}
}
トークンの分割コード
guard let fcmToken = Messaging.messaging().fcmToken else { return }
var params: [String: Any]? =[:]
fcmToken.split(99).enumerated().forEach {
params?["FCM_TOKEN_\($0.offset)"] = $0.element
}
後々、Cloud Functions側で受け取った際に連結します。
「分割したトークン」はこんな感じになります。
探しやすいように "FCM_TOKEN_" というプレフィックスと番号を付けています。
(が、こちらはお好みでお願いします)
4. コードの全体像
1~3を総括すると以下のようになります。
(クラス名とかは適当です。お好みでシングルトンにするなり肉なり焼くなりしてください。)
import FirebaseMessaging
import FirebaseAnalytics
final class Logger {
func logForTest(targetParams: [String: Any]) {
/// firebaseのトークンを取得
guard let fcmToken = Messaging.messaging().fcmToken else {
debugPrint("token is nil")
return
}
/// トークンを付与するために新しいパラメータを生成
var params: [String: Any]? = targetParams
/// firebaseのログでは、パラメータのValueに100文字制限があるため分割している
fcmToken.split(99).enumerated().forEach {
params?["FCM_TOKEN_\($0.offset)"] = $0.element
}
/// ログを送る
FirebaseAnalytics.Analytics.logEvent("TestLog", parameters: params)
}
}
もともと、出来上がっているログにトークンを差し込めるような仕組みにしています。
なので、ただ試したいだけの人は targetParams: [String: Any]
はなくても問題ありません。
補足
通常トークンはサーバーに保存するなりして、必要なユーザーIDに応じてそのトークンを取り出すような実装になります。なのでかなり無理矢理な実装をしています。
実際firebaseのsampleでも、Firebase Realtime Database
に保存したトークンを使用する例が紹介されています。
② 送られたログをCloud Functionsで検知
「ログの実装 + トークンの付与」は終わったので、Cloud Functions側でそれを検知していきます。
今回は、Node.js(JavaScript)
で実装していきます。
(Javascriptをちゃんと書くのは3年ぶりなので、コード上で良い書き方があればご指摘お願いします、、笑)
1. 環境構築
冒頭でも述べましたがこちらに関しては記載しません。
とはいえ、「ログのハンドリングまでの環境構築」を詳しく説明している記事があるので、以下を参考にするとよいでしょう。
以下、基本的にコード説明のみなので、試したい方はCloud Functionsにデプロイするのを忘れないでください。
2. 送られたログを検知
アプリ側で「TestLog」という名前で送ったので、受け取り側でも指定します。
const functions = require('firebase-functions');
const admin = require('firebase-admin');
admin.initializeApp();
exports.outputEventLogOfTestLog = functions.analytics.event('TestLog').onLog( event => {
return console.log(event);
});
上の3行はFirebase Cloud Functions
を使用する際のお作法みたいなものなものです。
下の3行がハンドリングと受け取った際の処理を記述しています。例としてconsole.log
を呼んでいます。
ログは実際にCloud Functionsの管理画面上でログを確認することできます。
うまく動作すれば、Cloud Functions上で定義したoutputEventLogOfTestLog
がFunctions上のログに現れるはずです。
(お見せできない黒塗りが多くてすいません。笑)
送られてきたログがCloud Functionsでフックできていることが確認できました。
③ Cloud Functionsで端末にPushを送る
1. ログからトークンを結合する
(Push通知に必要な)分割して送っているトークンをログから取り出し結合します。
そのためのスクリプトは以下になります。
function takeOutToken(params) {
return Object.keys(params)
.filter(key => {
return (key.search(/FCM_TOKEN_/) === 0);
})
.map(key => {
return params[key];
})
.join("");
}
FCM_TOKEN_
というプレフィックスがついたものだけを取り出して結合しています。
2. トークンからPush通知を送る
取り出したトークンからPushを送りたい端末を特定できるので、Push通知を送れる機構を作ります。
そのためのスクリプトは以下になります。
async function sendEventCallBack(tokens, params) {
// 返したい値をすべてStringに変換(でないとPush通知に付与できない)
var newParams = params
Object.keys(params).forEach(key => {
newParams[key] = String(params[key]);
})
// Pushの中身を生成
let payload = {
notification: {
title: 'callback', // Pushに表示されるタイトル
body: 'body', // Pushに表示される中身
},
data: newParams
};
// Pushを送信
return admin.messaging().sendToDevice(tokens, payload);
}
ここで着目すべき点としては、Push通知というのは
- タイトル
- サブタイトル
- 画像
などの**ユーザーに目に見える要素、以外のデータも送れる!**ということです。
それを利用して、data
というキーに送られてきたパラメータを付与します。
ただし、注意点としてStringでないと送れないという制約があるため、すべてStringにしています。
(この段階で返却する値の型が変わってしまうので、テストとしてはどうなのということがよぎっていた、、、笑)
titleとbodyは個人でわかるものをお好みでお願いします。
FirebaseのPush通史に関して詳しくみたい方は以下を参考にすると良いでしょう。
3. コードの全体像
今までのスクリプトコードを連結し、総括すると以下のようになります。
(最終的にCloud Functionsにデプロイするコードはこちらです)
const functions = require('firebase-functions');
const admin = require('firebase-admin');
admin.initializeApp();
/* triggers */
// 「TestLog」という名前のログを検知する
exports.outputEventLogOfTestLog = functions.analytics.event('TestLog').onLog( event => {
const params = event.params;
const tokens = takeOutToken(params);
if (tokens.length === 0) {
return console.log('error: tokens is empty');
} else {
return sendEventCallBack(tokens, params);
}
});
/* functions */
// ログからfcmトークンを取り出す
function takeOutToken(params) {
return Object.keys(params)
.filter(key => {
return (key.search(/FCM_TOKEN_/) === 0);
})
.map(key => {
return params[key];
})
.join("");
}
// Push通知を送る
async function sendEventCallBack(tokens, params) {
var newParams = params
Object.keys(params).forEach(key => {
newParams[key] = String(params[key]);
})
let payload = {
notification: {
title: 'callback',
body: 'body',
},
data: newParams
};
return admin.messaging().sendToDevice(tokens, payload);
}
念のために取り出したtokenのnullチェックを入れています。
Push通知の送信が成功した場合は、Functions上の管理画面に以下のようなログを出るはずです。
④ アプリ上でデータハンドリングする
ここまでくればもうお終いです。
Push通知が届けば完了ですが、裏側でもデータを取り出してみましょう。
といっても、デフォルトのプッシュ通知のDelegate(UNUserNotificationCenterDelegate
)を使うだけです。
extension AppDelegate: UNUserNotificationCenterDelegate {
func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
debugPrint(notification.request.content.userInfo) // データ格納場所
completionHandler([.badge, .sound, .alert])
}
}
ちなみにdidReceive
でも受け取れますが、
func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void)
**「通知がタップされたときの処理」**なので、Pushをタップしなくても取得できるwillPresent
で行うことをオススメします。
ということで、無事一連の動作を実装することがができました。
3. 問題点
結構これを実装するために試行錯誤したのですが、、、
Pushに遅延が発生することがわかり、この方法はログテストとしてのリアルタイム性は担保できないので、、、
結局採用しませんでした、、笑
FirebaseのPushは結局のところAppleのAPNsを経由するために、時々遅延が発生してしまうのです。
実際、この記事を書くためにもう一度手元で実装してみましたが、遅延がひどく、、、
そもそもテストとして色々ダメなところがあったので、実装している最後の方は、半ば興味本位で実装できるか試してた感じですね、、笑
参考
終わりに
所々、設定に関しては説明を省いているので、間に乗せている参考文献を良く読んでいただけると助かります。
例えば、
- 「firebaseの設定が24時間反映されない」
- 「発火するためのイベントは設定画面でオンにする必要がある」
とかがあるのですが、ここに書くと長くなってしまうので省略しており、本当に読んで欲しいです。笑
プロダクトとしては無駄になってしまいましたが、、、
Firebaseのいろんな機能を学べてクセとかも勉強になったので、これはこれでよかったかなと笑
長くなってしまいましたが、間違い等あればご指摘あれば助かります、、、!!