Webナイト宮崎 で Firestore 設計の話をしてきたよ #Webナイト宮崎
もう2週間ぐらい経ってしまいましたが、先日 Webナイト宮崎 というイベントで Firestore の話をしてきました!
Webナイト宮崎とは
宮崎のWeb系フリーランス集団 てげほげ が開催しているWeb系勉強会です。実は私もてげほげメンバーです!
以下、発表内容を簡単にまとめていきます。
発表内容
最初に今回話すことを整理。
Firestore におけるデータベース設計について自分が知ってる方法を話します。 この方法がベストプラクティスとは限らないので、もっと良い方法があったら教えてね!
Firestore の簡単なおさらい。すでに他の発表者の方も説明されていたので、ここはサラッと流しました。
今回は、とあるSNSサービスを例にDB設計について考えていきます。
このSNSサービスの機能要件です。とてもシンプルです。
最初にユーザー情報を格納するためのコレクションについて考えます。
今回は users
というコレクションを作り、その中に「1ユーザー1ドキュメント」という感じで追加していく感じにしました。
次に「ユーザーは他のユーザーをフォローすることができる」という仕様を Firestore に落とし込むときにどういう設計があるか?ということについて考えていきます。
いくつか方法はあると思いますが、今回は2つの方法をピックアップしました。
1つ目は users コレクション直下に follows
というサブコレクションを置く方法です。この例だと「aaaaa は bbbbb と ccccc をフォローしている」という状態になります。
2つ目は root 直下に follows
というフォロー情報を管理するための専用コレクションを作る方法です。この例だと「aaaaa は bbbbb をフォローしている」という状態になります。
さて、ここからは機能の実装に入っていきますよ。
まずは「aaaaaさんがフォロー中のユーザーリストを表示する」機能を実装するにはどうしたら良いでしょうか。
「/users 直下に置く」場合について考えましょう。 この例だと「aaaaa は bbbbb と ccccc をフォローしている」という状態になるので、ここは単純に赤枠を見れば良さそうです。
次に「/root 直下に置く」場合について考えましょう。 この例だと「aaaaa は bbbbb をフォローしている」という状態なので、followee = “aaaaa” のドキュメントを抽出すれば良さそうです(赤枠内)。
さて、今度は「aaaaaさんをフォローしているユーザーリスト(=つまりaaaaaさんのフォロワー)を表示する」機能を実装するにはどうしたら良いでしょうか。
こちらもそれぞれのパターンについて考えていきましょう。
まずは「/users 直下に置く」場合を考えてみます。
この例だと「bbbbb は aaaaa と ccccc をフォローしている」という状態になるので、
bbbbbさんはaaaaaさんのフォロワーということになります。
よって、users
コレクション内の follows
サブコレクションを横断して検索しながら、サブコレクションに aaaaa を含むドキュメントを抽出すれば良さそうです。
実はこれ、最近までできませんでした。この資料を作っている最中(2019年4月)に CollectionGroup
という機能がリリースされ、こういった横断的な検索も可能になりました。
- 待ち焦がれたCollectionGroupがCloud Firestoreへやってきた。 - Qiita - 1amageek さんの記事がとてもわかりやすいです
続いて「/root 直下に置く」場合について考えてみます。 この例だと「ccccc は aaaaa をフォローしている」という状態なので、ccccc は aaaaa のフォロワーということになります。よって、follower = “aaaaa” のドキュメントを抽出すれば良さそうです(赤枠内)。こちらは「/users 直下に置く」場合と比べるとだいぶシンプルですね。
最後にまとめ。今回は「フォロー/フォロワー」のようにリレーションシップを Firestore で管理する方法として /users 直下に置く方法と /root 直下に置く例を紹介しました。
/users 直下に置く方法だとサブコレクションを横断した検索ができない、という制約がありましたが、最近できるようになりました!
どちらの方法がベターか?という点については、格納するデータの内容によっても変わってくると思いますが、個人的には直感的にわかりやすい /root 直下に置くほうが好きです。
2019.6.3 追記:/root 直下に置く方法だと Collection の Write 制限で秒間に 500 人以上フォローできないことを 1amageek さんから教えていただきました!
この方法だとCollectionのWrite制限で秒間に500人以上フォローできないので、
— 1amageek (@1amageek) 2019年6月3日
SubCollectionに配置することをお勧めします。https://t.co/ZopF9xTP7E
感想
発表終了後にもたくさんの質問をいただき、割と好評だったという印象です。 Firestore は比較的新しいサービスであり、設計ノウハウなどもまだまだ不足しているなーと感じています。今後も勉強会などで積極的にアウトプットしていきたいですね!!
Webナイト宮崎、次回も参加します👍
Webナイト宮崎Vol.4今回も雰囲気最高だった!! LTも皆さんお疲れ様でした!!
— MICCI (@MICCI_0627) 2019年5月18日
リビルドのお二人もまた待ってます🌴🏄🏻♂️✈️ pic.twitter.com/uLtf1m3m40
▲てげほげmicciさんによるツイート!
発表スライド
- Firestore のデータ設計について - Speaker Deck - スライド全体はこちら
アルに入社しました
こんにちは koogawa です。この度、アル株式会社に入社しました。
アル株式会社について
マンガファンのためのサービス「アル」というサービスを運営する会社です。
入社の経緯
実は昨年の8月から副業として開発をお手伝いしていました。
しかし、上記エントリにも書きましたが、いつの間にか本業よりもアルでの仕事の方が楽しくなってしまったんですね。より学ぶことも多かったですし。なので思い切って本業を辞めました。
その後、しばらくフリーという立場でアルの開発をサポートしていたんですが「せっかくならフルコミットしたい」という気持ちが高まり、正式に加入することになりました。
本社は東京渋谷にあるので、自分は宮崎からのフルリモート勤務になります。
社内の印象
設立して間もない会社ということもあり、とにかく自由な雰囲気です。 「役割は誰かえらい人が与えられるものではなく、自分で見つけていくもの」 という考え方が軸にあり、基本的に仕事の指示もされません。みんな自分で考え、自分で行動しています。
もちろんプロダクトマネージャーなども存在せず、細かい仕様などはメンバーが主体となって決めていきます。
例えばこんなノリで機能が追加されていきます。メンバーを管理するリーダー等も存在しませんが、今のところ上手く回っています。
評価制度なんかも(まだ)存在しません。私個人としては期ごとに個人目標設を設定する作業はとても煩わしく感じますし、「今は評価の対象期間じゃないからその仕事はやらない」みたいな不毛なことが起こらずに済むのでとても動きやすいです。
時雨堂さんのエントリなんかはとても共感できます。
生活リズム
今の生活リズムをグラフにしてみました。
フルリモートですが、メリハリをつけるため雨の日以外はコワーキングスペースに通っています。
コアタイムも存在しないため、各自自分にあったスタイルで仕事をしています。自分にはまだ小さい子供がいるため、毎日18:00前には帰宅して子供の世話をできるのがありがたいです。家族からも「幸福度が上がった」と言われました。今までこんなことを言われたことなかったので、正直驚いてます。
アルのメンバー
現在、アルは代表の id:kensuu をはじめ、CTO id:wadap、VP of Product id:rinrin900、サーバーサイド兼インフラエンジニア id:astap、そして、iOSアプリエンジニア id:koogawa というメンバーで開発を進めております。 これからやっていきたいことはたくさんありますので、アルに興味を持ってくれた方がいればぜひ一度お話しましょう!
作っているもの
最後に宣伝も兼ねて今作っているプロダクトの紹介です。
自分はこのサービスのiOSアプリ開発をメインで担当しています。
アプリを Gmail と連携すると、過去に買ったマンガを分析し、新刊情報を通知で知らせてくれます。もちろん買ったことがないマンガもウォッチリストに追加することでウォッチの対象とすることができますよ!
アルのユーザーからたくさんウォッチされているマンガのリストも見られるので、次に読むべきマンガを簡単に見つけることもできます。
マンガ好きな方はぜひ使ってみてくださいね!
CarPlay対応アプリを雰囲気で作ってみる
これはなに
CarPlay対応アプリを開発する際の手順や、「できること/できないこと」をなんとなく理解するために、 とりあえず動くCarPlay対応のAudioアプリを作ってみたときの雑なメモです。
開発環境
- Xcode 10.1
- Swift 4.2
注意事項
実機(車載ナビ)でテストする場合は、下記URLからアップルへ連絡が必要になります。
https://developer.apple.com//contact/carplay/
1ヶ月ほど待つと、アップルから CarPlay Audio App Programming Guide がメールで送られてきます。このタイミングでデベロッパーアカウントにCarPlay entitlementがアサインされ、CarPlay対応アプリを実機で動かすことが許可されます。
なお、Simulatorでのテストだけであれば上記の連絡なしでもできるようです。
勘違いしていたこと
- CarPlay対応アプリは Watch App 等のようにターゲットを分けて作るわけではなかった
- AppDelegate を拡張して CarPlayでも動くようにするイメージ
- なので CarPlay 専用のアプリストアも存在しない
開発手順
1. Include the CarPlay audio app entitlement
Entitlements.plist
に com.apple.developer.playable-content
を追加します。
これを追加するだけでCarPlayのホーム画面に自分のアプリが表示されるようになります。
2. Show an app icon on the CarPlay home screen
CarPlay用のアプリアイコンをAssetsにセットします。
3. Extend AppDelegate
AppDelegate を拡張し、CarPlayに表示するビューコントローラをセットします。
@UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate { var window: UIWindow? var carWindow: UIWindow? func updateCarWindow() { guard let screen = UIScreen.screens.first(where: {$0.traitCollection.userInterfaceIdiom == .carPlay}) else { self.carWindow = nil return } // CarPlay is connected let carWindow = UIWindow(frame: screen.bounds) carWindow.screen = screen carWindow.makeKeyAndVisible() carWindow.rootViewController = CarViewController() self.carWindow = carWindow } func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { // Override point for customization after application launch. updateCarWindow() } 〜
CarPlay用の UIWindow
を別途用意し、その rootViewController
にビューコントローラをセットするイメージですね。
4. Present a hierarchical list to navigate and select audio content
曲リストを作るために MPPlayableContentDataSource
と MPPlayableContentDelegate
プロトコルを実装します。名前からわかるように前者はリストに表示するデータをセットし、後者は曲が選択された際のアクションを定義します。
class CarViewController: UIViewController, MPPlayableContentDataSource, MPPlayableContentDelegate { func numberOfChildItems(at indexPath: IndexPath) -> Int { return 3 } func contentItem(at indexPath: IndexPath) -> MPContentItem? { let item = MPContentItem.init(identifier: UUID.init().uuidString) item.title = "hoge rock" item.subtitle = "huga" item.isContainer = false item.isPlayable = true item.artwork = MPMediaItemArtwork(image: UIImage(named: "koogawa")!) return item } override func viewDidLoad() { super.viewDidLoad() // Do any additional setup after loading the view. MPPlayableContentManager.shared().dataSource = self MPPlayableContentManager.shared().delegate = self } func playableContentManager(_ contentManager: MPPlayableContentManager, initiatePlaybackOfContentItemAt indexPath: IndexPath, completionHandler: @escaping (Error?) -> Void) { // 曲が選択された! completionHandler(nil) }
今回は固定データを表示するだけですが、実際には動的にコンテンツを取得し、曲が選択された際にはネットワーク上の曲をストリーミング再生する、などの実装が必要になります。
実行方法
ここでいったんアプリを実行してみます。
iOS Simulatorのメニューから Hardware > External Displays > CarPlay でCarPlay用のSimulatorが起動します。
曲リストが表示されましたね!
がっつり実装したい場合は
今回、自分が実装したのはここまでですが、完全なオーディオアプリを開発する場合は次の実装も必要になります。
- 再生中の曲情報を表示する Now Playing screen に情報を提供する
- リモコンによるイベント(再生・停止・次の曲など)ハンドリング
詳細はアップルから送られてくるCarPlay Audio App Programming Guideを参照してください。
また、偶然GITHUB上で見つけた下記リポジトリも大変参考になりました。
資料
CarPlay Audio App 学び
CarPlay Audio App のサンプルを作ってる。曲リストの表示・選曲は MPPlayableContentDataSource, MPPlayableContentDelegate を実装するだけで UITableView っぽい UI を作ってくれる pic.twitter.com/KRlpin7sVL
— Og🌗エンジニア🏝宮崎 (@koogawa) 2019年1月13日
AppDelegate で CarPlay 用の UIWindow を用意してあげて、そいつのrootViewController に ViewController をセットするイメージ。気をつけないと AppDelegate がどんどん肥大化しそう pic.twitter.com/MhDuvsh2Pp
— Og🌗エンジニア🏝宮崎 (@koogawa) 2019年1月13日
セッション中に出てくるデモのホームボタンが右じゃなくて左にある理由がわかったぞ。向こうは基本的に左ハンドルだからだ。日本車は右ハンドルなのでホームボタンも逆(右)になる pic.twitter.com/eAqpiYdg9A
— Og🌗エンジニア🏝宮崎 (@koogawa) 2019年1月13日
CarPlay Audio UI(曲リストや再生中の画面)はテンプレートが用意されているのでそれを使えばよく、開発者は楽曲のタイトルなどの情報をAPIに提供するだけでよい、ということがわかった。 pic.twitter.com/MtiFy7ghzw
— Og🌗エンジニア🏝宮崎 (@koogawa) 2019年1月13日
CarPlay Map App についても様々なテンプレートが用意されている。地図の上に4つまでボタンを置いたり、別画面に8つまでのボタンを置いたり(Grid Template) pic.twitter.com/L7SGL2xgBE
— Og🌗エンジニア🏝宮崎 (@koogawa) 2019年1月13日
CapPlay App のカテゴリは
— Og🌗エンジニア🏝宮崎 (@koogawa) 2019年1月13日
- Automaker
- Messaging
- VoIP calling
- Audio
- Navigation
の5つなので、なんとなくチェックインアプリを作るのは無理そう、というところまでわかった。。https://t.co/66SOY1mL9n pic.twitter.com/KgiKlkWT3X
このビデオの中で紹介されている Automaker Apps の機能かもしれません。車種によっては外気温やガソリン残量なども取得できるみたいですが、詳しいドキュメントが見つかりませんでした(Appleに問い合わせないと貰えない類の資料かも)https://t.co/o0uLehCLeV pic.twitter.com/DDqKh0Nddl
— Og🌗エンジニア🏝宮崎 (@koogawa) 2019年1月13日
あぁホントですね・・!CarPlay接続時は車載機側のGPS使ってるの気付かなかった(今度iPhone側の位置情報機能オフにして実験してみよう)
— Og🌗エンジニア🏝宮崎 (@koogawa) 2019年1月13日
その後、実験してみた結果
試しにiPhoneの位置情報をオフにしてCarPlayを立ち上げてみたけど位置情報取得エラーになった。やっぱり車載側のGPSは使ってないのかもしれない
— Og🌗エンジニア🏝宮崎 (@koogawa) 2019年1月14日
プログラミングに関する情報を英語でググるときのコツ
あけましておめでとうございます!今年も本ブログをよろしくお願い致しやす🙏
さて、私はプログラミングに関する情報を調べる際、基本的に英語でググるようにしています。理由は単純で、英語のほうが圧倒的に情報量が多いからです。このエントリではいつも私が英語でググる際に使っているキーワードなどをメモしておきます。
- 実装した機能がうまく動かない場合
- 〜が表示されない
- メソッドなどが呼ばれない/実行されない
- プログラムで〜したい
- 2つの違いを知りたい
- 〜かどうかをチェックしたい
- 〜を検知したい
- 〜の実装方法を知りたい
- 〜についての最良の方法を知りたい
- あわせて読みたい
実装した機能がうまく動かない場合
〜 not work
でググります。「動く」だからといって move で検索しても期待した検索結果にはなりません。
さらにOSバージョン等を指定する際は
〜 not work in iOS 12
のように指定するとヒット率が上がります。*1
〜が表示されない
〜 not show (up)
メソッドなどが呼ばれない/実行されない
〜 not called
〜 not execute
プログラムで〜したい
programmatically
というキーワードを使う。
例えばiOSの開発で「Autolayoutの制約(constraints)をStoryboardからではなくプログラムコードで追加したい」場合は
autolayout constraints programmatically
なんかでググるとそれっぽいのがヒットする。
2つの違いを知りたい
日本でもよく使う vs
というキーワードが便利。
例えば REST における PUT と POST の違いについて知りたい場合は
PUT vs POST
のようにググるとそれっぽいのがヒットする。または普通に
difference PUT POST
のようにググるのも良い。
〜かどうかをチェックしたい
普通に check
というキーワードが使える。例えばある文字列が〜を含んでいるかどうかをチェックする方法を知りたい場合は
check if string contains 〜
のようにググってやるといい感じにヒットする。
〜を検知したい
例えば通信が切断されたタイミングを検知する方法を知りたい場合は
how to detect disconnection
のようにググると良い。
ユーザーが使用している OS のバージョンを検知する方法も
how to detect user os version
のようにググるとそれっぽいのがヒットする。
〜の実装方法を知りたい
How can I implement 〜
〜についての最良の方法を知りたい
best practices for 〜
あわせて読みたい
2018年を振り返る
このエントリは、今年一年の自己の振り返り Advent Calendar 2018 - Adventar の記事です。
どうも、koogawa です。今年も残りわずかですね。
今年のはじめに目標を立てていたので、ひとつずつ振り返ってみたいと思います。
目標振り返り
1. Stack Overflowで5000 reputationを目指す
達成しました🎉
実を言うと5月の時点ではまだ 4,000 reputation 程度でした。
今年の目標は 5,000 reputation なんだけど、今のままだとちょっとキツいかもしれん(;´ω`)
— Og🌗エンジニア🏝宮崎 (@koogawa) 2018年5月26日
しかし後半になると怒涛の追い上げを見せ、終わってみれば10月には目標の 5,000 に達していました。人間、追い込まれるとなんとかなるもんですな。
この調子で次は 10,000 reputation 目指します💪
2. 公開しているOSSのメンテナンスを続けていく
いくつかオープンソースのライブラリを公開していますが、しっかりメンテナンスも続けております。
- GitHub - koogawa/FoursquareAPIClient: Very simple Swift library for Foursquare API v2 - Foursquare API 用の軽量クライアント。公式デベロッパーサイトでも紹介されている
- GitHub - koogawa/LocationPickerController: Simple location picker with a built in maps. - 地図から緯度経度を取得できるUIライブラリ
- GitHub - koogawa/iSensorSwift: Sense a lot of things. (Swift ver) - 位置情報や輝度、音、速度など、iOSで検知できる機能を集めたSampler
今年公開された Swift 4.2 にももちろん対しております。
何れもニッチなライブラリですが、ちょっとでも使ってくれる人がいる限りメンテナンスは続けていきたいと思っています。
3. リモートでも参加できる勉強会に参加する
リモート・オフライン問わずいろんな勉強会に参加しました。
- PHPカンファレンス福岡2018
- iOSDC Japan 2018 前夜祭に参加してきたよ #iosdc - koogawa blog
- PHPカンファレンス福岡 2018 #phpconfuk に参加してきたよ - koogawa blog
- 宮崎 × 沖縄 JSやらNight! - connpass
- Webナイト宮崎 Vol.1 ~てげWeb学びたい~ ※10/9参加枠15→30名に増加 - connpass
昨年はまだ子供が小さかったこともあり、iOSDC への参加は見送っていたのですが、今年は一年ぶりに参加できました!楽しかったー😆
子育てとバランスを取りつつ、来年も可能な限りイベントに参加していく所存です。
おまけ
その他、今年あったイベントをちょっとだけ
息子が2歳に
移住当時は0歳だった息子が先日2歳になりました👦
宮崎の自然に囲まれ、 元気に育っております。
副業から本業へ
東京から宮崎へ移住後、しばらく宮崎を本拠地とする企業に正社員として勤めていたのですが、今年10月に退職しました。
退職理由としては、今年の夏ぐらいから初めた副業*1の方が楽しくなってしまったというのが正直な理由です。今はその副業が本業になっており、よりエキサイティングな毎日を送っております。
また、地元の会社文化に適応できなかったというのも理由の一つです。詳しい内容については割愛しますが、やはり地方には地方独特の文化や仕事の進め方があることを肌で感じました。もし地方への移住に興味があり、詳しい話を聞いてみたい方がおりましたら適当に声をかけてください。
◆
そんなわけで、来年もどうぞよろしくお願い致します!
*1:東京の案件です
【iOS/Swift】Firebase Cloud Firestoreで簡単なGPSロガーを作ってみる
この記事は 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 - 公式ドキュメント