koogawa blog

iOS、Android、foursquareに関する話題

【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:位置情報の利用をユーザーに許可して貰う必要があります。詳しい実装は全体のソースコードを参照してください

Xcode で Breakpoint を一括で削除する方法

割と知らない方が多かったのでメモ。

  1. 左のペインに Breakpoint Navigator を表示
  2. Workspace 右クリックして「Delete Breakpoints」選択
  3. これですべての Breakpoint が消える

image.png

頂いたフィードバック🙏

近況

こんにちは koogawa です。たまには近況報告なんかを書いてみます。

宮崎におります

普段からTwitterで宮崎のことをツイートをしまくってるので今さら感はありますが、「自然の多いところで子育てをしたい」という夢を叶えるために昨年、東京から宮崎に移住しました。

f:id:koogawa:20181025231134p:plain

長い電車通勤生活からも開放され、今は自転車・車中心の生活にシフトしております。

そんなわけで、現在は戸建ての物件を借りてのんびり暮らしております。騒音を気にしなくてよい、というのは良いことですね。

会社員 兼 個人事業主

今年の5月に Marimosoft という屋号を取得し、個人事業主になりました。

とは言っても完全な自由業というわけではなく、昼間は本業に集中し、夜は副業としてコードを書くというスタイルを取っています。睡眠時間を削る必要があるため、昼間は若干眠いですが、なんとか両立しています。

最近のお仕事

まだ詳しくは書けないのですが、わりと大きめのプロジェクトをお手伝いさせていただいております。自分はiOSをメインで担当しておりますが、非常に強いメンバーが集っており、刺激の多い日々を送っております💪早く公開したいな〜

副業の難しさ

会社員 兼 個人事業主として副業をやってますが、割とツラい面もあります。

日中は副業できない辛み

本業があるため、副業先で日中開催されるMTGに参加することが難しかったり、Slackに飛んできたリプライへの回答が遅れたりと、メンバーの皆さんにはいろいろと迷惑をかけてしまっていると思います🙇🏻

ただ、ちゃんとMTGの議事録は残してもらえますし、本業の休み時間を利用してSlackに返信したりもできますので、今のところなんとか続けられております。

眠み😪

帰宅後は、子供をお風呂に入れ寝かしつける、というタスクがあるため、副業を開始できるのはどうしても22時以降になってしまいます。そこから作業を始めると、眠りにつけるのはどんなに早くても午前0時過ぎになります。時には朝5時頃まで頑張ることもありました。そのため、昼間(特にランチ後)はちょっと眠くなるので、コーヒー飲んで頑張ってました☕

まとめ

いろんな課題はありつつも、意外と何とかなるもんです。しかし、最近はさすがに体力的にもキツくなってきたので、近い内に本業か副業のどちらか一本に絞りたいと考えています。

追記:その後、副業一本に絞りました。 現在はフリーな感じでやらせていただいております。めちゃめちゃ楽しいです💪

【Stack Overflow活動記】reputationが5,000に!Tag Wiki 編集の承認権限が付与されました

Stack Overflow活動中 の koogawa です、こんにちは。

昨日ついに reputation(Stack Overflowにおける信頼度)が 5,000 に到達しました🎉

f:id:koogawa:20181007020821p:plain

今年の初めに設定した目標 が「Stack Overflowで5000 reputationを目指す」だったので、無事達成できたことになります。

ちなみに、エンジニアアウトプットランキング Stargzr によると、日本で reputation 5,000 以上のユーザーは現在3人しかいないようです *1。ちなみに、1位の id:KishikawaKatsumi さんは 8,000 reputation を超えていらっしゃるので本当にすごいですね!

そして、同時に Approve Tag Wiki Edits 権限が付与されました。これは Tag Wiki の編集申請を承認できる権限になります。

Tag Wiki とは

Stack Overflow で質問する際には ios, swift, xcode などの Tag を設定することができます。

各々の Tag には専用のページが用意されており、Tag の概要文やその Tag における上位回答ユーザー、最近のベストな回答などが掲載されます。

f:id:koogawa:20181007022029p:plain

すべてのユーザーは Tag 概要文を編集することができますが、Approve Tag Wiki Edits 権限(今回私に付与された権限)を持つユーザーに承認されるまでサイトには反映されません。

Tag Wiki 編集が承認されるとどうなるか

初めて編集が承認されたユーザーは「Tag Editor」バッジ(銅)を獲得できます。

f:id:koogawa:20181007022042p:plain

さらに、50回編集が承認されると「Research Assistant」バッジ(銀)を獲得できます!

f:id:koogawa:20181007022053p:plain

次なる目標

次は 10,000 reputation で付与される Access To Moderator Tools 権限獲得を目指します💪

私の Stack Overflow 活動はこれからも続きますよ!

*1:もちろん実際はもっといるんでしょうけどね

iOSDC Japan 2018 前夜祭に参加してきたよ #iosdc

飛行機、電車を乗り継ぎ、iOSDC Japan 2018会場へ!2年ぶりの参加です。

ここは WWDC か!と思いました。

ビールもたくさんデプロイされていました。途中、ビールが足りなくなるというハプニングもありましたが、スタッフさんが近くのコンビニなどで補充してくれました。感謝しかない🙏

前夜祭では次のトークを聴講しました。

ひとつだけピックアップするとRyo Usamiさんの「標準アプリから学ぶ、HIGが教えてくれないiOSデザインのこと」がとても印象に残りました。

  • 広く利用されるものを利用する
  • 同じ見た目のものは場所が違えど同じふるまいをしよう
  • 新しい標準が登場したときはその背景、使い方を知ろう

という内容が一貫しており、みんな日常的に使っている「ドア」の例えが非常にしっくりきました。

「ドア」の使い方をあまり意識しないのは何度も使ってるからであり、「ドア」であれば同じふるまい・利用のされ方をするからである。そこに突然「同じドアなのに同じ動きをしない」ドアが現れると人は戸惑う、と。

***

ノベルティもたくさんいただきました!

今日も楽しんでいきます!

PHPカンファレンス福岡 2018 #phpconfuk に参加してきたよ

今年も行ってきました!PHPカンファレンス福岡2018!

phpcon.fukuoka.jp

今回で2回目の参加になります。

会場はおなじみ福岡ファッションビル

f:id:koogawa:20180616101710j:plain

朝5時半に高速バスで出発したにも関わらず、会場まで200km以上離れていたこともあり、ちょっと遅刻してしまいました。

今年もオシャレなトートバッグとTシャツを頂きました!嬉しい!

f:id:koogawa:20180616102229j:plain

(昨年頂いたTシャツも毎週のように着ています!)

ノベルティもたくさん頂いてしまいました。

f:id:koogawa:20180616124828j:plain

今年も勉強になるセッションが盛り沢山でした。そのうちいくつかをピックアップさせていただきたいと思います。

ログの設計してますか?PSR3とログ設計の話

登壇者:富所 亮(@hanhan1978)さん

このセッションでは「何」を「何故」「どのように」ログに残すのか、ログを出力することによるリスク、そしてログを出力する前に何を決めておけばよいのか?という話がメインでした。

個人的には、スピーディーに var_dump を書いたり消したりする新人さんの話がツボでしたw 自分もよくやっていたなーと。

未経験からの挑戦!超速ネイティブアプリ開発

登壇者:株式会社ハシゴ 松本 拓也(@skycat_me)さん

hasigo@ というモバイルアプリ開発のお話でした。技術選定として、iOS/Android エンジニアが不在だったため Flutter を採用されたそうです。最近 Flutter の採用事例が増えてきたなー、と興味深く聴かせていただきました。

エンジニアの循環ってgood_or_bad_.pdf

登壇者:中村剛(@nakamura_tsuyo4)さん

創業から2,3年、たくさんのエンジニアの入れ替わりを見てきた中村さんによる、エンジニアの流動性とそれをどう乗り切ってきているか、についてのお話でした。

個人的に印象的だったのが、退職者が続く場合、本人に辞める理由を聞いてみるのが一番だけど、辞めていく人は波風立てたくないので本音を語ってくれないよね、という話。その場合、出戻ってくる人がいるか?というのがひとつのバロメータになると中村さんは語っていました。私自身も退職者が後を絶たない環境にいたことがあるので、とても興味深く聴かせていただきました。

ソフトウェアエンジニアが英語に慣れ親しむ方法

登壇者:永冨隆之(@tommy6073)さん

初っ端の「日本に居ながら世界中にアクセスできるのがインターネットなのに、英語のコンテンツに触れないのは勿体無い」という話が心に刺さりました。ホントそのとおりですよね。

永冨さん個人の昔話で、海外ゲームを遊び尽くしたところ、TOEIC初受験で960点取れてしまった話はとても説得力があるものでした。私もなるべく普段から英語に触れるために Stack Overflow 活動なるものを進めているのですが、もっと頑張らないとなーと思いました。

PHP歴3か月だけど沖縄でフルリモート開発してる話

登壇者:嘉数 侑起(@kkznch)さん

スライドはまだアップされていないようですが、ビーチの上で開発するBDD(ビーチ駆動開発)というのが沖縄らしくて良いなーと感じましたw 波に乗りながら開発するスタイルも斬新でしたが、MacBookを落とさないかちょっと心配になりました😆

なぜPHPカンファレンスに参加するのか

私はスマートフォンアプリ開発を軸としているため、普段PHPは使いません。じゃあ、なんでPHPカンファレンスに毎年参加しているかというと

これが理由です。*1

実際、昨年このカンファレンスで知った Payment Request API や PWAなどの知識も現在役に立っています。

さいごに

PHPカンファレンス関係者の皆様、今年も楽しいイベントをありがとうございました!

f:id:koogawa:20180616180212j:plain

また来年会いましょう〜😄

*1:スティーブ・ジョブズのあの有名なスピーチに影響されてます

初心者をググれカスと突き放すのは割と酷なのではないかという話

こんにちは koogawa です。最近は Stack Overflow だけにとどまらず、エンジニア向けQAサイト teratail でも回答しております。teratail はスコアが上がっていくと色んなバッジをゲットできるので気に入っています。

teratail【テラテイル】|思考するエンジニアのためのQAプラットフォーム

ところで今日は次の質問に回答しました。

teratail.com

これ、めちゃくちゃ良い質問だと思うんです。何より自分がわからないことをちゃんと言語化できている。

すでに回答もいくつか付いていたので内容を見てみると。。

  • ぐぐる。これに尽きます
  • 普通にググれば見つかるのに
  • 何がわからんの?

だいたいこんな感じでした。

うーん、皆さんなかなか手厳しい回答です。。これだと質問者の方も、ここで挫折してしまうのではないでしょうか。

回答してみた

というわけで「自分ならどうググるか?」という視点で回答してみました。詳細は teratail のサイトでご覧ください。

https://teratail.com/questions/127346#reply-193795

(気づいたら1,600文字ぐらい書いていた・・)

結果的には喜んでもらえたようなので良かったんだと思います。

プログラミング初心者に僕らがしてあげられること

プログラミングに慣れてくると、わかることが当たり前の状態になってくるので、

  • なんでわからないのかわからない
  • とにかくググれ

的なことを言ってしまいがちです。しかし、初心者にとってはどうググって良いのかさえもわからないことが多いのです。

僕らに必要なのは「なぜわからないのか」を汲み取り、調べ方のコツなどを教えてあげることなのではないでしょうか。

経験者は1分で調べられるという誤解

質問者のコメントの中で印象的だったのが

経験者がピンポイントでドキュメントやらリファレンスを1分で調べてることを何時間もかけて調べてるんじゃないか?

と思い込んでいたことです。実際そうなのかというと、全くそんなことはないですよね。

ただ、ピンポイント率が上がるというのは確かにあるかもしれません。いろいろと調べているうちに、「ググり方」がうまくなっていく実感はあります。*1

***

まとめると、

  • 「ググれ」と突き放すのではなく、ググり方を教えてあげる
  • なぜわからないのか?を汲み取る

ということを書きました。プログラミング学習を挫折してしまう人がちょっとでも減ることを願っています。

今日書きたいことはそれくらいです。

*1:例えば「iOS ○○」ではなく「swift ○○」で検索したほうがプログラミングに関するサイトがヒットしやすい等