先日、サーバーサイドは全てFirebaseで担っているアプリ「シェアフィ」をリリースしました。
(制作期間は約5ヶ月、個人開発機関としては最長!)
開発したアプリ「シェアフィ」の紹介をしつつ、Firebaseの説明をしていきます。
今後の開発の参考にしていただければ、幸いです。
- アプリ「シェアフィ」について
- 使用Firebase
- FireStore
- FirebaseHosting
- FirebaseFunctions
- FirebaseCrashlytics
- AppDistribution
- FirebaseAnalytics
- CloudMessage
- RemoteConfig
- DynamicLinks
- 最後に
##アプリ「シェアフィ」について
簡単にアプリの説明です。
「シェアフィ」は、買い物メモを作成して、共有できるアプリになります。
特徴としては、
共有したら、相手にもリアルタイムでデータが反映される
→LINEとかTwitter見たいに勝手に更新される
データを更新したら、プッシュで相手に知らせてくれる
→LINEとかと同じ
この特徴を実装するにあたり、Firebaseが頑張ってくれています。
##使用Firebase
- FireStore
- FirebaseHosting
- FirebaseFunctions
- FirebaseCrashlytics
- AppDistribution
- FirebaseAnalytics
- CloudMessage
- RemoteConfig
- DynamicLinks
###用途
- FireStore
- メモの保存
- FirebaseHosting
- FAQなどのWEBページ(HTML+CSS+JS)
- FirebaseFunctions
- 連携した相手のプッシュトークン取得とリモートプッシュ
- FirebaseCrashlytics
- クラッシュ計測
- AppDistribution
- 公開前のデバッグのための配信
- FirebaseAnalytics
- アナリティクス
- CloudMessage
- リモートプッシュ通知
- RemoteConfig
- 強制アプデ用
- DynamicLinks
- 連携用の招待コード発行
##FireStore
###特徴
- リアルタイムで更新してくれる
- 「A」がデータを更新したら、「B」の表示されているデータも更新される
- 構造が[行,列]ではなくコレクションとドキュメント
DB構造が特殊のため、RDBに慣れている人は少々手こずるかもしれません。
上記設計になります。
サブコレクションは、ドキュメントの中に入れたコレクションです。
データ部分には、データ構造(例 {uuid:"000",name:"userName"})を入れています。
Listに関して、ドキュメントでは更新を検知できないため、コレクション→ドキュメント→サブコレクションという構造になっています。
###コード
class FirestoreManager {
let firestore = Firestore.firestore()
//ドキュメント以下のデータが更新されたら、その都度取得
func snapshotSelect() {
firestore.collection("collectionName").document("documentName").addSnapshotListener { snapshot, error in
guard let _snapshot = snapshot else {
return
}
print("Response:\(_snapshot)")
}
}
//1度だけの取得
func singleSelect() {
firestore.collection("collectionName").getDocuments() { queryDocuments, error in
guard let _queryDocuments = queryDocuments else {
return
}
print("Response:\(_queryDocuments.documents)")
}
}
//絞り込み(WHERE)
func whereSelect() {
firestore.collection("collectionName")
.whereField("keyName", isEqualTo: "value")
.getDocuments() { queryDocuments, error in
guard let _queryDocuments = queryDocuments else {
return
}
print("Response:\(_queryDocuments.documents)")
}
}
//データの追加(自動でドキュメント名設定)
func insertAutoDocumentName() {
firestore.collection("collectionName")
.addDocument(data: "setData") { error in
if let _error = error {
print("Error writing document: \(_error)")
}
else {
print("Document successfully written!")
}
}
}
//データの追加(主導でドキュメント名設定)
func insertAtDocumentName() {
firestore.collection("collectionName")
.document("docmentName").setData("setData") { error in
if let _error = error {
print("Error writing document: \(_error)")
} else {
print("Document successfully written!")
}
}
}
//データの更新
func update() {
firestore.collection("collectionName").document("documentName")
.setData("setData", merge: true) { error in
if let _error = error {
print("Error writing document: \(_error)")
} else {
print("Document successfully written!")
}
}
}
//データの削除
func deleteList(){
firestore.collection("collectionName").document("documentName").delete() { err in
if let err = err {
print("Error removing document: \(err)")
} else {
print("Document successfully removed!")
}
}
}
}
こんな感じになっています。
注意点としては、
リアルタイムでの更新をしたい場合は、「snapshot」でドキュメントを監視する。
** データの更新の時は「merge: true」を付ける**
の2点です。
###参考
Cloud Firestoreを実践投入するにあたって考えたこと
Firestoreを試してみた
iOSではじめてのFirestore [Swift]
##FirebaseHosting
###特徴
- サーバー用意せずともWEBページの公開ができる
###設計
シェアフィでは、HTMLとCSSとJSでFAQページを作成しました。
あとは、独自ドメインの適用してます。
###コマンド
#Firebaseへログイン
$ firebase login
#作業ディレクトリへ移動
$ cd dir
#初期化
$ firebase init
# 下記を選択
Hosting: Configure and deploy Firebase Hosting sites
Firebaseに複数プロジェクトがある場合は、任意のプロジェクトを選択
その後、色々聞かれますが、全て No を選択
# デプロイ
$ firebase deploy
###参考
【2018年版】利用前に知って欲しいFirebase Hostingにできること・できないこと
Firebaseで動かすNode.jsアプリ入門🔥
##FirebaseFunctions
###特徴
- プログラムだけで実行できる
- 環境構築の必要がない
- アプリから直接呼び出せる
###設計
リストを更新した際に共有相手にプッシュで知らせるために、使用しています。
コードは、Node.jsで記載
###コード
const admin = require('firebase-admin');
const functions = require('firebase-functions');
admin.initializeApp(functions.config().firebase);
let db = admin.firestore();
exports.pushSubmit = functions.https.onCall((request, response) => {
// パラメータの取得
const userId = request.useID;
const itemName = request.itemName;
// プッシュの内容
const payload = {
notification: {
title: "title",
body: "messege",
badge: "1",
sound:"default"
}
};
// Firestoreに検索
var pushRef = db.collection('collection').doc("documentName");
pushRef.get()
.then(doc => {
var members = doc.data();
for (var key in members) {
if (key != userId){
pushToDevice(members[key],payload);
}
}
})
.catch(err => {
throw new functions.https.HttpsError('invalid-argument', 'data.name is undefined.', err)
});
});
function pushToDevice(token, payload){
// priorityをhighにしとくと通知打つのが早くなる
const options = {
priority: "high",
};
admin.messaging().sendToDevice(token, payload, options)
.then(pushResponse => {
return { text: token };
})
.catch(error => {
// response.send(error);
throw new functions.https.HttpsError('unknown', error.message, error);
});
}
上記がFirebase側のコードになります。
アプリから受け取ったIDを元にFirestoreへアクセスし、プッシュの送り先を取得してます。
import Firebase
class CloudFunctionsManager {
let parameters = [
"useID": "useID",
"itemName": "itemName"
]
functions.httpsCallable("pushSubmit").call(parameters) { (res, error ) in
if let error = error {
print(error)
}
}
}
上記が、アプリからCloud Functionsのプログラムにアクセスするものになります。
APIやっているのにすごく短くて、簡潔です。
###参考
Firebase で Cloud Functions を簡単にはじめよう
Firebaseで動かすNode.jsアプリ入門🔥
##FirebaseCrashlytics
###特徴
- アプリ公開後のクラッシュ率を測れる
###設計
シェアフィでは公開後のバグ管理で使用しています。
無料で使用できるのが大きなメリット。
###注意点
最新のFirebaseCrashlyticsを使う場合では、ちょいちょいコードが変わっています。
import Crashlytics
↓
import FirebaseCrashlytics
Crashlytics.sharedInstance().crash()
↓
Crashlytics.crashlytics().fatalError()
その中でもハマったところをピックアップしました。
気をつけてください。
最新ではなく、「3.14.0」辺りの導入をお勧めします。
###参考
Firebase Crashlyticsを使ってみた
[Firebase][iOS] Firebase Crashlytics を導入してみたら環境で分けるのにハマった話
Firebase Crashlytics を使ってみる
Upgrade to the Firebase Crashlytics SDK
##AppDistribution
###特徴
- アプリ公開前に任意のユーザーに配布できる
- 複数人でのデバッグ
###設計
Fastlaneを使用して、ファイルのアップロードをしています。
###Fastlane導入
#インストール
$ cd Xcodeファイルのあるディレクトリへ(**.xcworkspace or **.xcodeprojがあるとこ)
$ gem install fastlane
#↑でダメだったら、
$ sudo gem install fastlane --verbose
#初期化
$ fastlane init
#fastlane のプラグインを追加しておく
$ fastlane add_plugin firebase_app_distribution
下記を参照に回答
iOSアプリのリリースフロー自動化ツールfastlaneの導入
作成されたディレクトリ「fastlane」→「Fastfile」
default_platform(:ios)
platform :ios do
desc "Description of what the lane does"
lane :app_distribution do
build_app(
scheme: "scheme",
export_options: {
method: "ad-hoc"
}
)
firebase_app_distribution(
app: "Firebaseのプロジェクトから",
groups: "作成したグループ名",
release_notes: "Lots of amazing new features to test out!"
)
end
after_all do |lane|
# 実行が成功したら
end
error do |lane, exception|
# 実行が失敗したら
end
end
ざっくりですが、「Fastfile」の中身になります。
###参考
Fastlane から Firebase App Distribution(Beta版) でテストアプリを配信する
[iOS] Firebase App Distributionを使用してiOSアプリを配布する
CircleCI + fastlane で Firebase App Distribution に ipa をアップロードする
fastlane を使用して iOS アプリをテスターに配布する
##FirebaseAnalytics
###特徴
- アプリ公開後の分析
- イベント数などに制限/クセがあるため、設計が重要
###設計
FirebaseAnalyticsは、だいぶ癖があります。
まず、収集できるものに対してかなり制限があります。
そして、管理画面から見れるデータにも制限があります。
詳しくはこちらから
収集と設定の上限
特に気をつけなければいけないのは、パラメータです。
収集の制限はありませんが、管理画面で閲覧できるのは、1プロジェクト50項目(テキスト10、数値40)だけです。
また登録できるイベントは500個までで、削除できません。
上記を踏まえた上でシェアフィでは、イベントではなく、パラメータで各種数値の計測を行うようにしています。
- 計測内容の一部
###コード
import Firebase
class FirebaseAnalytics {
// イベント
private func pushBaseAnalyticsEvent(eventName: String, param: [String : NSObject]){
Analytics.logEvent(eventName, parameters: param)
}
// ユーザープロフィール
private func pushBaseAnalyticsUserProfile(profilrName: String, value: String){
Analytics.setUserProperty(value, forName: profilrName)
}
}
計測を行うため実装は単純だと思います。
###参考
iOS 用 Google アナリティクスを使ってみる
Firebase Analyticsのイベントとパラメータをどう設定するべきか?
##CloudMessage
###特徴
- プッシュ通知機能
- 管理画面からと、プログラム経由がある
###設計
「CloudMessage」は、端末から取得できるFCMトークン(プッシュを送るために必要なトークン)を元にプッシュを送ります。
「CloudMessage」はグループ全体にプッシュを送ることや、AnalyticsとRemoteConfigと連携して特定のイベントを起こしたユーザーにだけプッシュを送るといったことができます。
シェアフィでは、プッシュ通知にて買い物を時間を伝えるローカルプッシュと買ったことを相手に知らせるリモートプッシュを使用しています。
「CloudMessage」はリモートプッシュで使用しています。
プッシュを送る相手がLINEのようにだいぶ特定された個人やグループになるため、「CloudMessage」のグループなどは使用できず、個別のFCMトークンを取得して、その人にだけプッシュを送るといったことを行なっています。
#####フロー
- アプリから「FirebaseFunctions」へUserIDを送る
- 「FirebaseFunctions」のUserIDから「FireStore」のPushコレクションを検索
- 取得したFCMトークンを「CloudMessage」に送り、プッシュを送る
###コード
FirebaseFunctions参照
###参考
(Firebase Cloud Messaging)[https://firebase.google.com/docs/cloud-messaging?hl=ja]
(LaravelでFirebase Cloud Messagingを使ってブラウザにプッシュ通知する)[https://qiita.com/kiyc/items/65ef447ca5f97bd3dad6]
##RemoteConfig
###特徴
- 管理画面でデータを入力し、アプリ側で取得する
- ABテスト
- アプリのバージョンを管理して強制アプデなどに使用
###設計
シェアフィでは、アプリの強制アップデートとして使用しています。
RemoteConfigにバージョンを登録して、インストールしているアプリのバージョンと比較し、RemoteConfigのバージョンの方が高かったら、アラートダイアログを出すようにしています。
ちなみにRemoteConfigでバージョン管理している理由は、アプリ公開後に任意タイミングでアップデートをさせたいからです。
###コード
import FirebaseRemoteConfig
class RemoteConfigManager {
static let shared = RemoteConfigManager()
let key = ""
let remoteConfig: RemoteConfig = RemoteConfig.remoteConfig()
var isUpdate: Bool {
guard let latestVersion = self.remoteConfig[key].stringValue else { return false }
return atof(latestVersion) > atof(AppConfig.appVersion())
}
func fetchLatestVersion(complete: (() -> Void)? = nil) {
self.remoteConfig.fetch(completionHandler: { [weak self] status, error in
if status == .success {
self?.remoteConfig.activate()
}
complete?()
})
}
}
アプリからRemoteConfigの値を取得するのが上記コードになります。
Key にRemoteConfigの管理画面に設定したKey名を設定します。
let alertController = UIAlertController(title: "アップデート", message: "ストアでアップデートしてください", preferredStyle: .alert)
let action = UIAlertAction(title: Localize.shared.toAppStore, style: .default, handler: { _ in
guard let url = URL(string: "itms-apps://itunes.apple.com/app/idアプリID") else { return }
UIApplication.shared.open(url)
})
alertController.addAction(action)
差分があった際は、上記のダイアログを表示しストアへ遷移させています。
###参考
FirebaseのRemoteConfig使ってみた
Firebase Remote ConfigのiOSへの導入
##DynamicLinks
###特徴
- 管理画面とプログラムから作成できる
- AndroidとiOSで共有のURL
- イメージは、ディファードディープリンクとディープリンクとユニバーサルリンクを混ぜたもの
###設計
「DynamicLinks」はディープリンクを持ったURLを作成してくれます。
シェアフィでは、ユーザー同士の連携部分で使用しています。
アプリをインストールしていれば、
アプリ起動→連携
未インストールなら、
ストアへ遷移→アプリをインストール→起動→連携
上記のフローを行えるため、採用し使用しています。
また、iOSとAndroidを分ける必要もないことも魅力の1つです。
###コード
// ユニバーサルリンクを使用するために
let url_str: String = "ユニバーサルリンクの対応したURL"
guard let link = URL(string: url_str) else { return }
// Firebase管理画面にて作成したURLでも
let dynamicLinksDomainURIPrefix = "https://〇〇.page.link"
let linkBuilder = DynamicLinkComponents(link: link, domainURIPrefix: dynamicLinksDomainURIPrefix)
linkBuilder?.iOSParameters = DynamicLinkIOSParameters(bundleID: "BundleID")
// 未インストール時にストアへ遷移するためにAppStoreID
linkBuilder?.iOSParameters?.appStoreID = "AppStoreID"
// 対応させるアプリの最小バージョン
linkBuilder?.iOSParameters?.minimumAppVersion = "AppVersion"
linkBuilder?.navigationInfoParameters = DynamicLinkNavigationInfoParameters()
// リンクタップ後、直接ストアへとぶか
linkBuilder?.navigationInfoParameters?.isForcedRedirectEnabled = false
guard let longDynamicLink = linkBuilder?.url else { return }
// 作成したURLを短縮URLへ変更
DynamicLinkComponents.shortenURL(longDynamicLink, options: .none, completion: { url, warnings, error in
guard let url = url else { return }
print(url)
})
###参考
[iOS] iOSアプリでFirebase Dynamic Linksを受け取る
FirebaseのDynamic Linksを使ってWebからアプリに誘導してみた
【iOS Firebase Dynamic Links】アプリインストールを経由するときに注意すること
最後に
最後までお付き合いいただき、ありがとうございました。
誤字脱字があったり、何か違っているところがありましたら、コメントや@gurensouenに、ご連絡いただけますと幸いです。
あと、ここの部分をより詳しく知りたい等がありましたら、ご連絡ください。
可能な限り対応(加筆)します。
これからもアプリの改善をしつつ、経験したことをアウトプットしていきます。
よろしくお願いします。
よかったら、アプリをインストールして、この部分はこのFirebaseの機能を使ってるんだ!
と、発見しつつ使ってみてください。
改めまして、ありがとうございました。
[]
(https://apps.apple.com/jp/app/%E3%82%B7%E3%82%A7%E3%82%A2%E3%83%95%E3%82%A3/id1506102636?mt=8)