koogawa blog

iOS、Android、foursquareに関する話題

Webナイト宮崎 で Firestore 設計の話をしてきたよ #Webナイト宮崎

f:id:koogawa:20190530131740p:plain

もう2週間ぐらい経ってしまいましたが、先日 Webナイト宮崎 というイベントで Firestore の話をしてきました!

Webナイト宮崎とは

tegehoge.connpass.com

宮崎のWeb系フリーランス集団 てげほげ が開催しているWeb系勉強会です。実は私もてげほげメンバーです!

以下、発表内容を簡単にまとめていきます。

発表内容

最初に今回話すことを整理。

Firestore におけるデータベース設計について自分が知ってる方法を話します。 この方法がベストプラクティスとは限らないので、もっと良い方法があったら教えてね!

f:id:koogawa:20190530142156p:plain

Firestore の簡単なおさらい。すでに他の発表者の方も説明されていたので、ここはサラッと流しました。

f:id:koogawa:20190530142226p:plain

今回は、とあるSNSサービスを例にDB設計について考えていきます。

f:id:koogawa:20190530142249p:plain

このSNSサービスの機能要件です。とてもシンプルです。

f:id:koogawa:20190530142305p:plain

最初にユーザー情報を格納するためのコレクションについて考えます。 今回は users というコレクションを作り、その中に「1ユーザー1ドキュメント」という感じで追加していく感じにしました。

f:id:koogawa:20190530142324p:plain

次に「ユーザーは他のユーザーをフォローすることができる」という仕様を Firestore に落とし込むときにどういう設計があるか?ということについて考えていきます。

f:id:koogawa:20190530142352p:plain

いくつか方法はあると思いますが、今回は2つの方法をピックアップしました。

1つ目は users コレクション直下に follows というサブコレクションを置く方法です。この例だと「aaaaa は bbbbb と ccccc をフォローしている」という状態になります。

f:id:koogawa:20190530142416p:plain

2つ目は root 直下に follows というフォロー情報を管理するための専用コレクションを作る方法です。この例だと「aaaaa は bbbbb をフォローしている」という状態になります。

f:id:koogawa:20190530142432p:plain

さて、ここからは機能の実装に入っていきますよ。

まずは「aaaaaさんがフォロー中のユーザーリストを表示する」機能を実装するにはどうしたら良いでしょうか。

f:id:koogawa:20190530142510p:plain

「/users 直下に置く」場合について考えましょう。 この例だと「aaaaa は bbbbb と ccccc をフォローしている」という状態になるので、ここは単純に赤枠を見れば良さそうです。

f:id:koogawa:20190530142539p:plain

次に「/root 直下に置く」場合について考えましょう。 この例だと「aaaaa は bbbbb をフォローしている」という状態なので、followee = “aaaaa” のドキュメントを抽出すれば良さそうです(赤枠内)。

f:id:koogawa:20190530142606p:plain

さて、今度は「aaaaaさんをフォローしているユーザーリスト(=つまりaaaaaさんのフォロワー)を表示する」機能を実装するにはどうしたら良いでしょうか。

f:id:koogawa:20190530142618p:plain

こちらもそれぞれのパターンについて考えていきましょう。

まずは「/users 直下に置く」場合を考えてみます。 この例だと「bbbbb は aaaaa と ccccc をフォローしている」という状態になるので、 bbbbbさんはaaaaaさんのフォロワーということになります。 よって、users コレクション内の follows サブコレクションを横断して検索しながら、サブコレクションに aaaaa を含むドキュメントを抽出すれば良さそうです。

実はこれ、最近までできませんでした。この資料を作っている最中(2019年4月)に CollectionGroup という機能がリリースされ、こういった横断的な検索も可能になりました。

f:id:koogawa:20190530142701p:plain

続いて「/root 直下に置く」場合について考えてみます。 この例だと「ccccc は aaaaa をフォローしている」という状態なので、ccccc は aaaaa のフォロワーということになります。よって、follower = “aaaaa” のドキュメントを抽出すれば良さそうです(赤枠内)。こちらは「/users 直下に置く」場合と比べるとだいぶシンプルですね。

f:id:koogawa:20190530142725p:plain

最後にまとめ。今回は「フォロー/フォロワー」のようにリレーションシップを Firestore で管理する方法として /users 直下に置く方法と /root 直下に置く例を紹介しました。

/users 直下に置く方法だとサブコレクションを横断した検索ができない、という制約がありましたが、最近できるようになりました!

どちらの方法がベターか?という点については、格納するデータの内容によっても変わってくると思いますが、個人的には直感的にわかりやすい /root 直下に置くほうが好きです。

f:id:koogawa:20190530142743p:plain

2019.6.3 追記:/root 直下に置く方法だと Collection の Write 制限で秒間に 500 人以上フォローできないことを 1amageek さんから教えていただきました!

感想

発表終了後にもたくさんの質問をいただき、割と好評だったという印象です。 Firestore は比較的新しいサービスであり、設計ノウハウなどもまだまだ不足しているなーと感じています。今後も勉強会などで積極的にアウトプットしていきたいですね!!

Webナイト宮崎、次回も参加します👍

▲てげほげmicciさんによるツイート!

発表スライド

アルに入社しました

f:id:koogawa:20190701112725j:plain
この写真は 2019.6.25 に撮ったものです

こんにちは koogawa です。この度、アル株式会社に入社しました。

アル株式会社について

マンガファンのためのサービス「アル」というサービスを運営する会社です。

alu.jp

入社の経緯

実は昨年の8月から副業として開発をお手伝いしていました。

しかし、上記エントリにも書きましたが、いつの間にか本業よりもアルでの仕事の方が楽しくなってしまったんですね。より学ぶことも多かったですし。なので思い切って本業を辞めました。

その後、しばらくフリーという立場でアルの開発をサポートしていたんですが「せっかくならフルコミットしたい」という気持ちが高まり、正式に加入することになりました。

本社は東京渋谷にあるので、自分は宮崎からのフルリモート勤務になります。

社内の印象

設立して間もない会社ということもあり、とにかく自由な雰囲気です。 「役割は誰かえらい人が与えられるものではなく、自分で見つけていくもの」 という考え方が軸にあり、基本的に仕事の指示もされません。みんな自分で考え、自分で行動しています。

もちろんプロダクトマネージャーなども存在せず、細かい仕様などはメンバーが主体となって決めていきます。

f:id:koogawa:20181219005438p:plain:w320

例えばこんなノリで機能が追加されていきます。メンバーを管理するリーダー等も存在しませんが、今のところ上手く回っています。

評価制度なんかも(まだ)存在しません。私個人としては期ごとに個人目標設を設定する作業はとても煩わしく感じますし、「今は評価の対象期間じゃないからその仕事はやらない」みたいな不毛なことが起こらずに済むのでとても動きやすいです。

時雨堂さんのエントリなんかはとても共感できます。

生活リズム

今の生活リズムをグラフにしてみました。

f:id:koogawa:20190131092138p:plain
https://tool.stabucky.com/maker/twentyfour/

フルリモートですが、メリハリをつけるため雨の日以外はコワーキングスペースに通っています。

コアタイムも存在しないため、各自自分にあったスタイルで仕事をしています。自分にはまだ小さい子供がいるため、毎日18:00前には帰宅して子供の世話をできるのがありがたいです。家族からも「幸福度が上がった」と言われました。今までこんなことを言われたことなかったので、正直驚いてます。

アルのメンバー

現在、アルは代表の id:kensuu をはじめ、CTO id:wadap、VP of Product id:rinrin900、サーバーサイド兼インフラエンジニア id:astap、そして、iOSアプリエンジニア id:koogawa というメンバーで開発を進めております。 これからやっていきたいことはたくさんありますので、アルに興味を持ってくれた方がいればぜひ一度お話しましょう!

作っているもの

最後に宣伝も兼ねて今作っているプロダクトの紹介です。

www.itmedia.co.jp

自分はこのサービスのiOSアプリ開発をメインで担当しています。

アプリを Gmail と連携すると、過去に買ったマンガを分析し、新刊情報を通知で知らせてくれます。もちろん買ったことがないマンガもウォッチリストに追加することでウォッチの対象とすることができますよ!

アルのユーザーからたくさんウォッチされているマンガのリストも見られるので、次に読むべきマンガを簡単に見つけることもできます。

マンガ好きな方はぜひ使ってみてくださいね!

CarPlay対応アプリを雰囲気で作ってみる

これはなに

CarPlay対応アプリを開発する際の手順や、「できること/できないこと」をなんとなく理解するために、 とりあえず動くCarPlay対応のAudioアプリを作ってみたときの雑なメモです。

開発環境

注意事項

実機(車載ナビ)でテストする場合は、下記URLからアップルへ連絡が必要になります。

https://developer.apple.com//contact/carplay/

1ヶ月ほど待つと、アップルから CarPlay Audio App Programming Guide がメールで送られてきます。このタイミングでデベロッパーアカウントにCarPlay entitlementがアサインされ、CarPlay対応アプリを実機で動かすことが許可されます。

f:id:koogawa:20190115001824p:plain

なお、Simulatorでのテストだけであれば上記の連絡なしでもできるようです。

勘違いしていたこと

  • CarPlay対応アプリは Watch App 等のようにターゲットを分けて作るわけではなかった
  • AppDelegate を拡張して CarPlayでも動くようにするイメージ
  • なので CarPlay 専用のアプリストアも存在しない

開発手順

1. Include the CarPlay audio app entitlement

Entitlements.plistcom.apple.developer.playable-content を追加します。

f:id:koogawa:20190115001912p:plain

これを追加するだけでCarPlayのホーム画面に自分のアプリが表示されるようになります。

2. Show an app icon on the CarPlay home screen

CarPlay用のアプリアイコンをAssetsにセットします。

f:id:koogawa:20190115001927p:plain

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

曲リストを作るために MPPlayableContentDataSourceMPPlayableContentDelegate プロトコルを実装します。名前からわかるように前者はリストに表示するデータをセットし、後者は曲が選択された際のアクションを定義します。

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)
    }

今回は固定データを表示するだけですが、実際には動的にコンテンツを取得し、曲が選択された際にはネットワーク上の曲をストリーミング再生する、などの実装が必要になります。

実行方法

ここでいったんアプリを実行してみます。

f:id:koogawa:20190115002158p:plain

iOS Simulatorのメニューから Hardware > External Displays > CarPlayCarPlay用のSimulatorが起動します。

f:id:koogawa:20190115002217p:plain

曲リストが表示されましたね!

がっつり実装したい場合は

今回、自分が実装したのはここまでですが、完全なオーディオアプリを開発する場合は次の実装も必要になります。

  • 再生中の曲情報を表示する Now Playing screen に情報を提供する
  • リモコンによるイベント(再生・停止・次の曲など)ハンドリング

詳細はアップルから送られてくるCarPlay Audio App Programming Guideを参照してください。

また、偶然GITHUB上で見つけた下記リポジトリも大変参考になりました。

資料

CarPlay Audio App 学び

その後、実験してみた結果

プログラミングに関する情報を英語でググるときのコツ

あけましておめでとうございます!今年も本ブログをよろしくお願い致しやす🙏

さて、私はプログラミングに関する情報を調べる際、基本的に英語でググるようにしています。理由は単純で、英語のほうが圧倒的に情報量が多いからです。このエントリではいつも私が英語でググる際に使っているキーワードなどをメモしておきます。

実装した機能がうまく動かない場合

  • 〜 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 〜

あわせて読みたい

blog.koogawa.com

*1:in ではなく on iOS X って書く人も多いので、どちらが正しいのかはわかっていない

2018年を振り返る

このエントリは、今年一年の自己の振り返り Advent Calendar 2018 - Adventar の記事です。


どうも、koogawa です。今年も残りわずかですね。

宮崎の空
宮崎の空

今年のはじめに目標を立てていたので、ひとつずつ振り返ってみたいと思います。

blog.koogawa.com

目標振り返り

1. Stack Overflowで5000 reputationを目指す

達成しました🎉

blog.koogawa.com

実を言うと5月の時点ではまだ 4,000 reputation 程度でした。

しかし後半になると怒涛の追い上げを見せ、終わってみれば10月には目標の 5,000 に達していました。人間、追い込まれるとなんとかなるもんですな。

この調子で次は 10,000 reputation 目指します💪

2. 公開しているOSSのメンテナンスを続けていく

いくつかオープンソースのライブラリを公開していますが、しっかりメンテナンスも続けております。

今年公開された Swift 4.2 にももちろん対しております。

何れもニッチなライブラリですが、ちょっとでも使ってくれる人がいる限りメンテナンスは続けていきたいと思っています。

3. リモートでも参加できる勉強会に参加する

リモート・オフライン問わずいろんな勉強会に参加しました。

昨年はまだ子供が小さかったこともあり、iOSDC への参加は見送っていたのですが、今年は一年ぶりに参加できました!楽しかったー😆

子育てとバランスを取りつつ、来年も可能な限りイベントに参加していく所存です。

おまけ

その他、今年あったイベントをちょっとだけ

息子が2歳に

移住当時は0歳だった息子が先日2歳になりました👦

f:id:koogawa:20181201223649p:plain
みやざき臨海公園はいつも空いている

宮崎の自然に囲まれ、 元気に育っております。

副業から本業へ

東京から宮崎へ移住後、しばらく宮崎を本拠地とする企業に正社員として勤めていたのですが、今年10月に退職しました。

退職理由としては、今年の夏ぐらいから初めた副業*1の方が楽しくなってしまったというのが正直な理由です。今はその副業が本業になっており、よりエキサイティングな毎日を送っております。

また、地元の会社文化に適応できなかったというのも理由の一つです。詳しい内容については割愛しますが、やはり地方には地方独特の文化や仕事の進め方があることを肌で感じました。もし地方への移住に興味があり、詳しい話を聞いてみたい方がおりましたら適当に声をかけてください。

そんなわけで、来年もどうぞよろしくお願い致します!

*1:東京の案件です

【iOS/Swift】Firebase Cloud Firestoreで簡単なGPSロガーを作ってみる

この記事は Firebase #2 Advent Calendar 2018 4日目の記事です。


Firebase を理解するには何か作ってみるのが一番!ってことで、今回は簡単なGPSロガーを作ってみました。

f:id:koogawa:20150802150835p:plain

次のような機能があります。

  • 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/ から新規プロジェクトを作成します。

f:id:koogawa:20181203231855p:plain

チェック項目については各自おまかせします。

テータベースの作成

左のメニューから「Database」を選択し、「データベースの作成」ボタンをクリックします。

f:id:koogawa:20181203233644p:plain

Cloud Firestore データモデルについて

公式ドキュメントから引用します。

Cloud Firestore は NoSQL ドキュメント指向データベースです。SQL データベースとは違い、テーブルや行はありません。代わりに、データは「ドキュメント」に格納し、それが「コレクション」にまとめられます。

これは図に表すと理解しやすいと思います。

f:id:koogawa:20181203232958p:plain

また、各「ドキュメント」には、一連のキーと値のペア(フィールド)が含まれています。今回は locations コレクションにGPSログ(ドキュメント)を追加していく想定で進めていきます。

f:id:koogawa:20181203233919p:plain

iOSアプリに Firebase 追加

iOS」ボタンをクリックします。

f:id:koogawa:20181203233955p:plain

バンドル名は com.example.GPSLogger、アプリ名は GPSLogger としました。

f:id:koogawa:20181203234015p:plain

GoogleService-Info.plist をプロジェクトに追加

画面に従っていくと GoogleService-Info.plist をダウンロードするように促されるので、これをプロジェクトに追加します。

f:id:koogawa:20181203234303p:plain

このファイルには 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()
}

ここではイベントを受け取るたび(つまり位置情報が追加されるたび)にデータをロードし、テーブルビューを更新しています。includeMetadataChangestrue にしているのは、データが削除された際にもイベントを受け取るためです。

Stopボタンを押したときの処理

位置情報の取得を停止します。

self.locationManager.stopUpdatingLocation()

また、リアルタイムアップデートも停止します。先ほど実行した addSnapshotListener メソッドの戻り値 ListenerRegistrationremove メソッドを呼ぶことで、イベントの受け取りを停止します。

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 を使い慣れている方にとってはツッコミどころ満載だと思いますので、ご指摘など歓迎です:-)

github.com

リンク

*1:位置情報の利用をユーザーに許可して貰う必要があります。詳しい実装は全体のソースコードを参照してください