21
16

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

FirebaseAdvent Calendar 2020

Day 13

SwiftとFirebaseでアプリをリリースした話 〜Firebaseについて〜

Last updated at Posted at 2020-05-10

先日、サーバーサイドは全てFirebaseで担っているアプリ「シェアフィ」をリリースしました。
(制作期間は約5ヶ月、個人開発機関としては最長!)

開発したアプリ「シェアフィ」の紹介をしつつ、Firebaseの説明をしていきます。
今後の開発の参考にしていただければ、幸いです。

##アプリ「シェアフィ」について
簡単にアプリの説明です。
「シェアフィ」は、買い物メモを作成して、共有できるアプリになります。
特徴としては、
 共有したら、相手にもリアルタイムでデータが反映される
  →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に慣れている人は少々手こずるかもしれません。

###設計
シェアフィでは、
スクリーンショット 2020-05-08 18.49.07.png

上記設計になります。
サブコレクションは、ドキュメントの中に入れたコレクションです。
データ部分には、データ構造(例 {uuid:"000",name:"userName"})を入れています。
Listに関して、ドキュメントでは更新を検知できないため、コレクション→ドキュメント→サブコレクションという構造になっています。

###コード

FirestoreManager.swift
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
スクリーンショット 2020-05-10 14.17.34.png
# 下記を選択
Hosting: Configure and deploy Firebase Hosting sites

Firebaseに複数プロジェクトがある場合は、任意のプロジェクトを選択
その後、色々聞かれますが、全て No を選択

# デプロイ
$ firebase deploy

###参考
【2018年版】利用前に知って欲しいFirebase Hostingにできること・できないこと
Firebaseで動かすNode.jsアプリ入門🔥

##FirebaseFunctions
###特徴

  • プログラムだけで実行できる
    • 環境構築の必要がない
  • アプリから直接呼び出せる

###設計
リストを更新した際に共有相手にプッシュで知らせるために、使用しています。
コードは、Node.jsで記載

###コード

Push.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へアクセスし、プッシュの送り先を取得してます。

CloudFunctionsManager.swift
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個までで、削除できません。

上記を踏まえた上でシェアフィでは、イベントではなく、パラメータで各種数値の計測を行うようにしています。

  • 計測内容の一部
スクリーンショット 2020-05-10 19.30.28.png - 実際の管理画面 スクリーンショット 2020-05-10 19.33.12.png

###コード

FirebaseAnalytics.swift
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トークンを取得して、その人にだけプッシュを送るといったことを行なっています。

#####フロー

  1. アプリから「FirebaseFunctions」へUserIDを送る
  2. 「FirebaseFunctions」のUserIDから「FireStore」のPushコレクションを検索
  3. 取得した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でバージョン管理している理由は、アプリ公開後に任意タイミングでアップデートをさせたいからです。
###コード

RemoteConfigManager.swift

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名を設定します。

Alert.swift
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つです。

###コード

CreateDynamicLinks.swift
// ユニバーサルリンクを使用するために
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の機能を使ってるんだ!
と、発見しつつ使ってみてください。

改めまして、ありがとうございました。

[APPICON]
(https://apps.apple.com/jp/app/%E3%82%B7%E3%82%A7%E3%82%A2%E3%83%95%E3%82%A3/id1506102636?mt=8)

icon-40@3x.png

21
16
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
21
16

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?