この記事は Firebase #2 Advent Calendar 2018 4日目の記事です。
Firebase を理解するには何か作ってみるのが一番!ってことで、今回は簡単なGPSロガーを作ってみました。
次のような機能があります。
- Startボタンを押すと位置情報を記録開始
- アプリをバックグラウンドに落としても記録し続ける
- 位置情報が取得されると地図にもピンが立つ
- 1日経過したデータは起動時に自動削除
- Stopボタンを押すと位置情報の取得終了
***
以下、実装メモです。すべての実装は説明できないので、完全なソースコードは最後の方に貼ってある GitHub リポジトリを参照してください。また、今回は学習を目的としたサンプルプログラムという位置付けなので、料金については詳しく触れません。Cloud Firestore はデータの読み取り、書き込み、削除の回数によっても課金されますので、下記のリンクもよくお読みください。
Cloud Firestore の料金 | Firebase
動作環境
- Xcode 10.1
- Swift 4.2
- FirebaseCore 5.1.8
- Firebase/Firestore 5.13.0
- CocoaPods 1.5.3
手順
プロジェクト作成
https://console.firebase.google.com/ から新規プロジェクトを作成します。
チェック項目については各自おまかせします。
テータベースの作成
左のメニューから「Database」を選択し、「データベースの作成」ボタンをクリックします。
Cloud Firestore データモデルについて
公式ドキュメントから引用します。
Cloud Firestore は NoSQL ドキュメント指向データベースです。SQL データベースとは違い、テーブルや行はありません。代わりに、データは「ドキュメント」に格納し、それが「コレクション」にまとめられます。
これは図に表すと理解しやすいと思います。
また、各「ドキュメント」には、一連のキーと値のペア(フィールド)が含まれています。今回は locations
コレクションにGPSログ(ドキュメント)を追加していく想定で進めていきます。
iOSアプリに Firebase 追加
「iOS」ボタンをクリックします。
バンドル名は com.example.GPSLogger
、アプリ名は GPSLogger
としました。
GoogleService-Info.plist をプロジェクトに追加
画面に従っていくと GoogleService-Info.plist
をダウンロードするように促されるので、これをプロジェクトに追加します。
このファイルには Firebase を使うためのIDなどがセットされています。このファイルを追加しないとアプリ起動時にクラッシュします。
Cloud Firestore インストール
次のような Podfile を用意して pod install
します。
use_frameworks! target 'GPSLogger' do pod 'Firebase/Core' pod 'Firebase/Firestore' end
アプリで Firebase を初期化する
AppDelegate で FirebaseApp.configure()
を実行し、アプリを初期化します。
import UIKit import Firebase @UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate { var window: UIWindow? func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { FirebaseApp.configure() return true } }
モデル作成
位置情報を格納するためのモデルを作ります。緯度、経度、作成日時のみを保持するようにしました。
struct Location { let latitude: Double let longitude: Double let createdAt: Date init(document: [String: Any]) { latitude = document["latitude"] as? Double ?? 0 longitude = document["longitude"] as? Double ?? 0 createdAt = document["createdAt"] as? Date ?? Date() } }
緯度経度を格納するための型(地理的座標)も用意されていますが、今回はわかりやすくするためにあえて Double
型を採用しています。
後述しますが、Firebase からはデータが Dictionary
型で返ってくるので、init(document: [String: Any]) {
のようなメソッドを定義し、そこから Location
インスタンスを生成できるようにしています。
Firebase では次の型が使用できます:配列、ブール型、バイト、日時、浮動小数点数、地理的座標、整数、マップ、Null、参照、テキスト文字列。
Startボタンを押したときの処理
位置情報の取得を開始します。*1
self.locationManager.startUpdatingLocation()
位置情報を追加
取得できた位置情報(CLLocation
)を Firestore に追加します。
let db = Firestore.firestore() var ref: DocumentReference? = nil ref = db.collection(kLocationsCollectionName).addDocument(data: [ "latitude": location.coordinate.latitude, "longitude": location.coordinate.longitude, "createdAt": FieldValue.serverTimestamp() ]) { err in if let err = err { print("Error adding document: \(err)") } else { print("Document added with ID: \(ref!.documentID)") } }
追加日時は FieldValue.serverTimestamp()
メソッドで時間軸を全てサーバーに預けてしまうことによってモバイルの個体差によるずれを解消しています。
リアルタイムアップデート
addSnapshotListener
メソッドを使用すると、ドキュメントが更新されたときにイベントを受け取ることができます。
let db = Firestore.firestore() self.listener = db.collection("locations") .addSnapshotListener(includeMetadataChanges: true) { [weak self] documentSnapshot, error in guard let document = documentSnapshot else { print("Error fetching document: \(error!)") return } print("Current data: \(document.description)") self?.loadStoredLocations() }
ここではイベントを受け取るたび(つまり位置情報が追加されるたび)にデータをロードし、テーブルビューを更新しています。includeMetadataChanges
を true
にしているのは、データが削除された際にもイベントを受け取るためです。
Stopボタンを押したときの処理
位置情報の取得を停止します。
self.locationManager.stopUpdatingLocation()
また、リアルタイムアップデートも停止します。先ほど実行した addSnapshotListener
メソッドの戻り値 ListenerRegistration
の remove
メソッドを呼ぶことで、イベントの受け取りを停止します。
self.listener.remove()
アプリを起動したときの処理
getDocuments
メソッドを実行して Firestore に保存されている位置情報をロードします。
let db = Firestore.firestore() db.collection(”locations”) .order(by: "createdAt", descending: false) .getDocuments { [weak self] snapshot, error in if let error = error { print("Error getting documents: \(error)") } else { self?.locations = snapshot?.documents.map { Location(document: $0.data()) } ?? [] } }
createdAt
でソートをかけて全データを取得し、Location
型の配列としてローカルに持ちます。
古いデータの削除
古いデータがいつまでも残ってしまうのを防ぐため、1日経過した位置情報ログを削除します。
まずは whereField
を利用して1日(86400秒)より古いデータを抽出します。
let db = Firestore.firestore() db.collection("locations") .whereField("createdAt", isLessThanOrEqualTo: Date().addingTimeInterval(-86400)) .getDocuments { snapshot, error in if let error = error { print("Error getting documents: \(error)") return } for document in snapshot?.documents ?? [] { print("Deleting document", document) self.delete(documentID: document.documentID) } }
そして一件ずつ delete
メソッドで削除していきます。
fileprivate func delete(documentID: String) { let db = Firestore.firestore() db.collection("locations") .document(documentID) .delete() { err in if let err = err { print("Error removing document: \(err)") } else { print("Document successfully removed!") } } }
大量にデータを削除する場合はメモリ不足エラーを避けるため、小さなバッチに分けてドキュメントを削除することが推奨されています。
所感
割と簡単にGPSロガーが作れてしまいました。ドキュメントが充実しているのも安心できますね。
とくに便利なのがオフラインデータ機能です。この機能により、アプリが使用している Cloud Firestore データのコピーがキャッシュに保存されるため、端末がオフラインの場合でもアプリはデータにアクセスできます。端末がオンラインに戻ると、アプリがローカルで行った変更とリモートの Cloud Firestore に保存されたデータが同期されます。なんて素敵な機能なんでしょう!
ソースコード
こちらに全てアップしてあります。Firebase を使い慣れている方にとってはツッコミどころ満載だと思いますので、ご指摘など歓迎です:-)
リンク
- Cloud Firestore | Firebase Documentation - 公式ドキュメント