自分を攻略していく記録

自分がやりたいことを達成するには何をすればいいのか、その攻略していく過程をつらつらと

OYO RoomsとWeWorkとHomeAwayに見るローカライズの戦略

インドのOYO Roomsがめちゃくちゃキテる

OYOというインドのホテルを扱うスタートアップが$1billion調達したと話題になっていた。しかも代表は自分と同じ24歳。ざっくり言うとアジア版の安いAirbnbみたいなイメージだが、これまでのホテル会社と比べて圧倒的に伸びている。しかも拠点であるインド以外の、中国、マレーシア、ネパールといった東南アジアで急激に伸びている。特に、中国では、10ヶ月で171都市、87,000部屋のリスティングという異常な伸び方だったという。OYOは買収も積極的に行なっており、アパート管理会社のNovascotia Boutique Homesや、スマートホテルをやっているIoT会社のAblePlus、ウェディングを扱っているWeddingzを買収している。

inc42.com

中国では、华住酒店集団と提携を結んだりと、ローカライズが非常にうまくいっている。

WeWorkのローカライズは現地の会社の買収でうまくやってきた

海外に展開する上でのWeWorkのローカライズについて見てみる。WeWorkがシンガポールにやってきた時、現地のSpacemobというコワーキングスペースのスタートアップを買収した。このSpacemobは設立が2016年6月で、買収されたのはその1年2ヶ月後という超スピードExitである。現地の伸びている強豪を買収することでWeWorkは東南アジアでも加速していった。

thebridge.jp

travelmobの超速Exit

travelmobという会社は2012年の7月に設立されてその1年後に、HomeAwayというアメリカのバケーション向け滞在場所のマーケットプレイスに買収されている。こちらも超速Exitだ。travelmobは買収されてHomeAwayのアジア側のローカライズの役目を果たしている。HomeAwayは、中国の途家(tujia)とも提携して中国でのローカライズにも注力していた。

techcrunch.com

まとめ

東南アジアでグローバルに展開している会社は、若くても伸びているスタートアップを買収してそのマーケットに参入するのが顕著。早い会社は1年でもExitしている。とにかく早い。

参考

https://www.medianama.com/2018/06/223-oyo-expands-operations-in-china-now-present-in-26-cities/

https://inc42.com/buzz/oyo-marks-its-third-buyout-in-2018-with-online-marketplace-weddingz/

https://techcrunch.com/2016/11/23/spacemob/

ベトナムに行ってきた

f:id:ngo275:20180924151810p:plain

基本情報

  • 首都 ハノイ
  • 人口 約9000万人
  • 面積 日本の0.88倍
  • 言語 ベトナム語
  • 通貨 ドン
  • 平均年齢 30歳弱
  • GDP 2500億USD (日本は5兆USD)

GDP成長率は6%程度、インフレ率が4%程度で成長が著しい。ただ、物価は日本の3分の1程度。社会主義国ドイモイ政策によって経済が発展したと言われている。

首都ハノイホーチミンとの交通の便が非常に悪く輸送には4日かかることが普通だそう。鉄道も老朽化が進み、道路の渋滞もひどいため、経済発展にはインフラが大きな課題となっている。

行って感じたこと

感想まとめ

- バイク量すさまじく物流が課題すぎる。先日やってきたGo-Jekに期待
- シェアリングエコノミーがまだまだ発展していない。評価システムとかクレジットがまだあまりなさそう
- 物価が急に上がったせいか、安い or 高いの二極化しておりローカルの人は経済的に大変そう

この週末にベトナムホーチミンに行ってきた。噂には聞いていたが、バイク王国ぶりにはビックリした。自転車がほぼ皆無で、基本的な移動手段はバイク、次いで自動車になる。もちろんタクシーはあるが、現地の人はバイクの後ろに乗せてもらって移動するのが一般的なようだ(先月からGo-Jekというインドネシア発のバイクタクシーがベトナムでも展開され始めたそう)。なので、空気は綺麗でなく道路も先進国と比べると汚いという印象を受けた。9月は雨季ということもあり、日中は天気が崩れがちだったが、みんなレインコートを着用してでもバイクに乗る。車線を無視した運転は当たり前で、クラクションでお互い事故らないように気をつけ合っていた。初めは「なんて危険なんだ」と思ったが、道が混みすぎて自動車の速度は基本20~30km/hなので、事故っても怪我で済む程度なのだろう。多分、日本において、歩道を利用する自転車と歩行者みたいなイメージかなーと勝手に思った(歩道って自転車のベルは鳴らすけど、車線の概念がなくて無法地帯だし)。一応、電動自転車という乗り物もあるそうだが、日本におけるそれとは異なり、むしろ原付に近いようだ。

f:id:ngo275:20180925223951j:plain:w500

インフラが課題とは聞いていたが、移動手段が豊富になりつつある今の時代なら都心にあらゆるものを集積する必要はなく、分散的な街をデザインできればインフラ問題から解放されると思う。ただ、現状は移動手段が固定されすぎており集積せざるを得なくなっている。

中国をはじめとした諸国ではシェアサイクルが流行っているが(もう下火ではあるものの...)、ベトナムでは一切、そういったものを見かけなかった。彼らは子供の頃からバイクに乗せてもらって過ごすそうなので、人力で漕ぐ乗り物をわざわざ使わないのだろう。自転車へのスイッチイングコストが高すぎる。バイクの保有率は全世帯の9割に登るようで今さら自転車には戻れない。

シェアサイクルの他にも、アメリカを中心に電動キックボードが流行っているが、これは人力ではないので自転車よりはいいかもしれない。ただ、歩道はそこまで整備されていなかったり、雨が多く、水たまりもできやすい、という問題は残っており、こうした課題を鑑みると、電動自転車でもいいのではないかと思える。

結局、このモビリティ問題で重要なのは、バイクなのか・自動車なのか・電動キックボードなのか・電動自転車なのか、という手段よりは、 シェアリングという概念がそこまで浸透していない のが大きいと思える。中国やシンガポールに比べてシェアリングエコノミーが進んでいないのである。

先日、世界の信用についてのデータが発表されていたが、中国では圧倒的なクレジットが生まれており、GDPの発展の1つに寄与してるだろう。たしかに、リーマンショックの頃と同程度に膨れ上がっているので今後が不安ではあるが、ベトナム(ホーチミンしか行っていないが)ではクレジットがもっと必要なように感じた。中国ではその信用スコアの評価システムがあったので、それと連携して色々なものがシェアリングされていったが、そういった土壌がまだないように感じた。

f:id:ngo275:20180925221925p:plain:w400

あとは、ローカルの人が食べているものはめちゃくちゃ安い(100円とか)だが、観光客向けの食べ物になると600円とかかかるので、ローカルの人からすると、物価が高いか安いかの二極化をしてるんだろうなと思ったし、そのせいかお金に対してシビアに映った。写真はグエン・フエ大通り公園と呼ばれるところで、夜10時でも盛り上がっていて、楽しい街だった。

f:id:ngo275:20180925223916j:plain:w500

goo.gl

Ethereumについて詳しく勉強する時に参考になるサイト

Ethereumを触ってスマートコントラクトを書くことはあっても、その仕組みを詳しく学ぶのが後回しになってしまっていた。その時に参考にしたサイトが以下の通り。

Yellow paperや下のMediumの記事の日本語版で非常に丁寧に書かれてある。

coffeetimes.hatenadiary.jp

上の記事の元になっているMediumの記事。英語だが、Yellow paperを丁寧に説明したものになっている。

medium.com

EthereumのWhite paperで平易な英語で書かれている。これとYellow paperを見つつ、一番最初にあげた日本語の記事を参照すると良いかもしれない。

github.com

以下のpdfは図式で非常にわかりやすくまとめられていた。

http://takenobu-hs.github.io/downloads/ethereum_evm_illustrated.pdf

あとは、以下のosukeさんのサイトや、Gunosyのブログが参考になった。

ZOOM | ブロックチェーンと仮想通貨の情報サイト

blockchain.gunosy.io

シンガポールの移動にはシェアサイクルが超便利 ~SG Bikeの使い方~

f:id:ngo275:20180826012641j:plain:w500

昨日からシンガポールに滞在している。移動するときには、タクシーか、タクシーの配車アプリGrabが便利だが、ちょっとした移動には自転車が便利だったので紹介する。

シンガポールのいたるところには同じ自転車が停まっており、それは中国のシェアサイクルと同様に好きなところで借りて好きなところで乗り捨て可能な自転車である。中国発のofoもあったが、SG Bikeという自転車が最も広まってるように感じた。

せっかくなので僕も使ってみることにした。

f:id:ngo275:20180826014019p:plain:w240

その場でSG Bikeというアプリをダウンロード・会員登録して、クレジットカードを登録し、デポジットを入れた。10SGD(約800円)から入金可能で、35分借りて1.15SGD(約100円)だった。他にも、10SGDで90日間使い放題のプランもあるようだった。中国のシェアサイクルと全く同じ感じだが、街の規模自体が中国ほど大きくないので、大体の移動はシェアサイクルでどうにかなる感はある。

アプリのウォレット画面 自転車のQRを読み込んで解錠している様子 台数がめちゃくちゃある
f:id:ngo275:20180826012720p:plain:w300 f:id:ngo275:20180826013109p:plain:w300 f:id:ngo275:20180826013227p:plain:w300

ただし、会員登録には電話番号が必要なのでSIMカードが必要になった。Grabも同様に現地で会員登録する場合はSIMカードが必要になるので注意が必要そう。こういったアプリを日本でダウンロードして会員登録しておけば問題ないが、今回のように現地でアカウント開設すると慣れていない場合は苦戦するかもしれない。シンガポールSIMカードを手に入れるには、シンガポールの空港の到着ロビーで購入するのが手っ取り早いだろう。

Ethereumにコマンドラインからスマートコントラクトをデプロイする

f:id:ngo275:20180821093307p:plain

はじめに

最近Parity v2がリリースされた。以前までParityは、 http://localhost:8545 でアクセスするとウェブのUIが出てきて、そこでコントラクトの実行やデプロイもできたのだが、v1.10からはそれ用のアプリを落として来る必要が出てきた(もちろんウォレットの互換性はある)。しかも監視しているコントラクト一覧のタブが消えてまったく違うところに移動していたり、謎のバグに遭遇したりとかなり辛い...。

簡単なコントラクトであればParityのUI上でデプロイしても問題ないと思うが、がっつり開発していくとなると、コマンドラインからtestnetやmainnetにデプロイしたくなる。ここではParityを使ってkovanにコントラクトをデプロイしていく(mainnetもkovanも基本的にやり方は変わらない)。

github.com

Parity側の準備

以下Macを想定する。

# Parityをインストールしていなかった場合
$ brew install parity --devel

# Parityのv1系がすでにインストールしてあった場合
$ brew upgrade parity --devel

次に以下のリンクからParity UIをインストールする。すでにインストールしてあったものとコンフリクトする場合は、 sudo rm -rf /Applications/Parity\ UI.app を実行してからインストールする。すでにインストールしてある場合はスキップして問題ない。ただし、バージョンの互換性が著しくひどいので、もしその場合はアップグレードしておく。

github.com

Parityの起動

$ parity --chain kovan --jsonrpc-apis "eth,net,web3,personal,parity" --geth

上記のコマンドでParityが起動する。 --chain kovan と指定することで、テストネットのkovanにつながる。初めて実行する場合、ブロックの同期に数時間はかかるので注意が必要。

f:id:ngo275:20180821092714p:plain

コントラクト側の準備

簡単なコントラクトをTruffleを使って作成する。

github.com

[Ethereum]$ mkdir deploy-sample
[Ethereum]$ cd deploy-sample/
[deploy-sample]$ truffle init
Downloading...
Unpacking...
Setting up...
Unbox successful. Sweet!

Commands:

  Compile:        truffle compile
  Migrate:        truffle migrate
  Test contracts: truffle test
[deploy-sample]$
[deploy-sample]$ truffle create contract Sample

truffle.js にnetworkの設定を書いておく。

// truffle.js

module.exports = {
  // See <http://truffleframework.com/docs/advanced/configuration>
  // to customize your Truffle configuration!
  networks: {
    development: {
      host: "127.0.0.1",
      port: 8545,
      network_id: 42,
      from: "0x002fA73D59421a7ec5525821c9C9f6BfCbf634E9", // replace this with your address
      gas: 5000000
    }
  }
};

デプロイする

web3.toHex(180) で180秒間このアカウントをアンロックしている(手元で確認した時は、アンロック状態でParityを触るとなぜかうまくコントラクトが実行できなかったので180くらいがいいかもしれない)。

$ truffle console

> web3.personal.unlockAccount('0x002fA73D59421a7ec5525821c9C9f6BfCbf634E9', 'YOUR_PASSWORD', web3.toHex(180));
> true
# コントロール+Cで抜ける

$ truffle migrate --reset

これでコントラクトがkovanにデプロイされているはず。

Firebase Meetup #5 で登壇してきた

プロトタイピングするならFirebaseを使ってみよう @ngo275

f:id:ngo275:20180807231041p:plain:w400

自分のLT。IoTを使って得たデータをiOSアプリでリアルタイム表示する、という開発を先日したのでそれについて話をした。プロトタイプを作るときは、データの扱いやログイン部分・ファイルアップロードとかAPIをなるべく書かずに、アプリ側の実装に集中したいところだが、Firebaseだとそれができるので良かった。いくらサーバーもクライアントも書ける人でもFirestoreとかを使うともっと早く実装できるはず。

speakerdeck.com

はじめてのCloud Firestore su-さん

su-さんの話で、前回ブログ枠だったが今回はLT。クックパッドiOSエンジニアで、komercoでFirebaseをがっつり利用したということもあり非常に詳しかった。Firestoreは従来のデータベースとは異なるが、設計時にリストかサブコレクションなのか、バックアップをどうするのか、とか実務をする上での話が聞けた。

speakerdeck.com

Firestore のクエリと全文検索 @miup

FirestoreのクエリはRealtime Databaseよりは改善されたが、実際にプロダクションでFirestoreを使うのはまだまだ苦しい...。

5000円以上、10000円以下で、在庫がない というのは検索可能だが、 5000円以上、10000円以下で、在庫がある という検索はFirestoreだけだと実現できない。複数のフィールドに対して範囲を絞った検索ができないため、 在庫があるstock > 0 とフィルターかけようとすると、うまくできないためらしい。それを回避するにはAlgoriaを使うことになるが、そういったシチュエーションにフォーカスした話。

speakerdeck.com

Firestoreを本番運用して得た知見と事件簿 @Vexus2

Firestoreでアプリをリリースして運用しており、その中で得た知見とやらかした話。2ヶ月に2度DBを空にしてしまったそうだが、落とし穴について実体験に基づいていて面白かった。まだ公式にバックアップやリストア機能がないのでOSSで出ているexport機能を使うことになりそうで、Googleさんに期待。

(スライドは上がり次第追加

Firebase Unity SDKを触ってみよう! @grem_ito

UnityでFirebaseを触る時の導入で、なかなかウェブにはiOSAndroidの話はあってもUnityの話は少ないので参考になる話だった。VR上でアプリを作っていくにはUnityが必要そうだしキャッチアップしたいと思いながらもついつい後回しにしてしまうところ...。

docs.google.com

まとめ

Firebaseの初心者向けの回だったこともあり、マニアックな話は少なかった。むしろ実運用上での知見が多くとてもためになった。実際にプロダクションで運用している人もちらほら増えてきているようなので、実際に運用してみたツラミとか良かったことのシェアが今後期待できそう。Firestoreを使っている人が多く、Realtime Databaseの話が一切なかったのだが、みんなバックアップ・リストアができない問題とかどうしているのかが気になった。Realtime Databaseはオワコンなのだろうか...。

個人で作ったiOSアプリを公開した ~マッハリーダー~

f:id:ngo275:20180805112939g:plain:w300

5月くらいから暇な時にコツコツ作っていたアプリを公開した。コメントやハイライトができるPDFリーダーで、そのコメントやハイライトは他のユーザからも見れる、というもの。 PDFをアップロードして、他のユーザも同じものを見ていると、他のユーザのアクションが見れるのだ。 (以下のアイコンからダウンロードできます)

ソースコードも全部公開しています。Firebaseの設定ファイルはコミットしていないので欲しい方いらっしゃれば個別に連絡ください。

github.com

なぜ作ったのか

本の著者と読者のコミュニケーションはあまりに一方通行 なのが気になっていた。ウェブサービスだと、サービスプロバイダとユーザは双方向のコミュニケーションが当然のように行なうことができ、もしサービスにバグがあれば、サービスプロバイダは速攻で修正し修正版をユーザにすぐに届けることが可能だ。ところが、「本」に関しては、本を読んだ感想だったり疑問に思ったことを読者が著者にフィードバックする方法が特にないし、もし本に誤りがあった場合、それを読者に届けるには買い直してもらう必要がある。たしかに、電子書籍であれば修正版をすぐに届けることは可能だが、コミュニケーションはそれでも一方通行だ。 思うことをコメントしてそれを他のユーザが読めたり、著者が簡単にフィードバックを得る場があればいいな と思ったので作ることにした。

少し前までは本を書くのは素人には難しかったが、今なら誰でも簡単に書くことができるようになってきた。もちろん紙の本は今でもそう簡単ではないが、電子書籍であれば誰でも出版可能だ。まだまだ電子書籍を出すのも一部の人に限られているが、もっと増えてくればいいと思い、それに合わせたリーダーも作れればいいと思っている。

なぜPDFにしたのか

電子書籍デファイアントスタンダードの形式はPDFではなく、 ePub である。iOSePubリーダーを出しているOSSもあり、それを採用するつもりで、実はこのOSSコメント機能を実装してPRも出したりもしたのだが、ePub描画はWebviewでゴリゴリ実装する必要があったり、機能要件を考慮するとこのePubOSSを流用するのではなく0から作る必要がありそうだったり、PDFならiOS標準でビューワーが提供されている、といった理由から一旦PDFで出してみることにした。

github.com

どう実装しているのか

アプリ側のアーキテクチャ

アプリはSwiftでMVVM (Model・View・ViewModel)で書いたつもり。View(ViewController)は描画やユーザからの入力だけを意識しており、Viewに1対1対応でViewModelも作った。ViewModelにはModelを整形したり、そのViewに依存するModelの処理を書き、Modelはデータベースの利用を表現している。

サーバレスなバックエンド

バックエンドの実装にはFirebaseを最大限利用した。

Firestoreの利用にはPringというモデルフレームワークを利用した。これを使うとModelを定義して、そのまま model.save をするとFirestoreにいい感じにオブジェクトが保存される。また、リレーションシップにも対応しているのでモデル間の関係性も表現できる。Firestoreはクエリ検索が貧弱なので、データ構造はViewに依存する形にしている。

Firebaseまわりで完全にバックエンドを作っているサービスはまだ少なく、情報が少ないのでFirestoreまわりで結構苦戦した。このアプリのリポジトリは公開しているので是非参考にしていただけると嬉しい(間違いも指摘していただけるとなお嬉しいです)。

アプリ間のやりとり

他のアプリでPDFを開いている時に、シェアアクションの中からこのアプリを選択して保存しておく、だったりアプリを起動して読む、ということも実装しようとした。結論から言えばShareExtensionで前者は実装したが、後者は具体的な方法がわからず未実装になっている。

developer.apple.com

まとめ

ePubに対応する、PDFの更新に対応する、他のアプリがいい感じで起動する、といった機能は未実装なので今後実装していきたい。もし手伝ってくれる方がいらっしゃればぜひ手伝ってください🙇‍♂️。

blockchain.tokyo #10で登壇してきた

blockchain.tokyoとは

f:id:ngo275:20180728185557p:plain:w400

blockchain.tokyoは、ブロックチェーン技術の普及や、ナレッジの共有を目的としたコミュニティで、グノシーやメルカリ、DMMが主体となって運営している。毎月100人規模で開催されており、話の内容も技術的なものが非常に多いコミュニティ。

blockchain-tokyo.connpass.com

今回は10回目で「Ethereumの基礎」がテーマだった。

Ethereumで開発を始めるなら知っておきたいこと

自分 @ngo275が発表してきた。対象は、WebエンジニアやアプリエンジニアだがEthereumやDAppsについてはあまり知らないという人を想定した。今までの開発とは技術的にも思想的にも異なる部分が多いので、それについても取り上げて話してみた。

speakerdeck.com

開発者的にはEthereumを使う、となるとDAppsを作りたくなるが、いろいろ整理していくとDAppsで事業を作っていくのはかなり困難が多そうに感じる。たとえば、OmiseGoのようにコミュニティ活動を積極的に行って、Ethereumそのものを良くすることが必要になる、とか。ただ、昨年ではICOが非常に流行ったが、それはICOのようにトークンを確実にセキュアにばらまくことはEthereumですでに可能だったから、という理由がある。最近ではセキュリティトークンが話題になっているが、その方面ではスケーラビリティはそこまで大きな問題ではなく、ICOと同様に、技術的な課題が事業のボトルネックになることはなさそうに感じる。知れば知るほどEthereumの課題の多さに驚いてしまうが(Vitalik氏もオモチャと言っている)、現状のEthereumでできることは少しずつ見えてきているのも事実だ。

Ethereumのデータ構造

グノシーのエンジニア サルバドールさんの発表。マークルパトリシアツリーの説明から入って、Storageがどのように保存されているのか、最後にRLPデコードをする話だった。かなりコアなところまで踏み込んでいった話で面白かった。gethの内部で利用されているLebelDBの扱いが難しかったそう。

speakerdeck.com

Ethereumを支えるネットワークの話

DMMのエンジニア さんの発表。Ethereumのプロトコルは大きくDEVp2pとEthereum Wire Protocolがあるが、その前者(ネットワーク部分)の話で、ノード探索・接続の方法についてわかりやすくまとめてあった。この説明を聞いた後にweb3でJSON-RPCで接続する、と聞くと理解が進むかもしれない。

speakerdeck.com

Gasを誰が払うのか問題について

ゆで卵さんのLTで、Ethereumのガスの扱いについて。これは非常に頭が痛い問題で、この発表にあるようにユーザに負担させないようにする提案(ERC877)、ETH以外のトークンで払えるようにする提案(ERC865)、そういったのをまとめるインターフェースの提案(ERC1077)がある。ここまで普段見ていなかったので勉強になった。

speakerdeck.com

State Channelエコシステムと実用上の課題

グノシーのStateChannelの神こと中村くんのLT。たとえばState Channelをチェスに応用をした時に一つコマを動かすたびに署名を求める必要がでてくるよう。State Channelによるスケーラビリティ解決は、技術的に実現できそうな一方で、まだまだ詰めるべき点があるので今後もウォッチしておいた方が良さそうだ。

speakerdeck.com

以下で詳しく説明されている。

blockchain.gunosy.io

Ethereum fast sync

最後はころさんのLT。ブロックの同期に時間がかかってしまう問題についてわかりやすくまとめてある。ここもあまり知らなかったので勉強になった。

speakerdeck.com

まとめ

「Ethereumの基礎」というテーマだったらしいが、基礎的な話というより低レイヤーな基盤についての話が多かった。勉強になる話が多くてよかった。

Firebaseでサーバレスアプリをサクッと作ってみる② ~Firestore実装とプッシュ通知連携~

以下の記事でFirebaseのセットアップからプッシュ通知を受け取るところまで実装したので、次はFirestoreにデータを書き込み、それをトリガーにしたCloud Functions経由でプッシュ通知を送信するように実装していく。

diary.shuichi.tech

Firestoreをセットアップする

Podfilepod 'Firebase/Firestore' / pod 'Pring' / pod 'Firebase/Auth' と追加して pod install を実行する。Xcodeのバージョンが9.2以下だとうまく動かなかったので、もしXcodeのバージョンが低い場合はアップグレードする必要がある。Pringを利用すると直感的にモデルを扱えるので今回はこれを利用する。Firestoreにデータを書き込むためには、認証が必要なので Firebase/Auth も利用する(Firestoreのセキュリティルールをゆるくすれば必要はないがせっかくなので認証も実装する)。

Pringのリンクはこちら。わかりやすいので個人的には気に入っている。

github.com

ただ、使い方が若干わかりにくいので、この記事を参考にしていただければ嬉しい。

diary.shuichi.tech

続いてFirebaseのコンソールに行って、Databaseの初期化をする。

f:id:ngo275:20180710004034p:plain:w500

Databaseのルールを開き、下の画像のように編集する。このセキュリティルールが甘いせいでユーザの情報が流出する事件が先日起こったのでよく注意しよう。

f:id:ngo275:20180710010733p:plain:w500

Firebase Authenticationも利用するので、初期設定をしていく。

f:id:ngo275:20180710011035p:plain:w500

匿名の欄を開いて有効化する。ここの設定を忘れるとうまくいかないのだが、忘れがちなので気を付けるべきポイント。

f:id:ngo275:20180710011149p:plain:w500

モデルを実装していく

新しく Model.swift を作成して以下のようにObjectを継承したUserとBookを宣言する。本来ならそれぞれ User.swiftBook.swift のように分割する方が綺麗かもしれないがここではまとめて書いておく。 User については、匿名ログイン時に発行されるIDを使ってデータを生成し、すでに User が生成済みであれば何もしないようにしている。 UserBook の関係は、 User has many Books で、 NestedCollection を利用するとデータを取得する時に後々効いてくる。

// Model.swift

import Foundation
import Pring

@objcMembers
class User: Object {
    dynamic var name: String?
    dynamic var fcmToken: String?
    dynamic var books: NestedCollection<Book> = []

    static func anonymousLogin(_ completionHandler: @escaping ((User?) -> Void)) {
        Auth.auth().signInAnonymously { (auth, error) in
            if let error = error {
                print(error.localizedDescription)
                completionHandler(nil)
                return
            }
            
            guard let currentUser = Auth.auth().currentUser else {
                completionHandler(nil)
                return
            }

            User.get(currentUser.uid) { (user, _) in
                if let user = user {
                    print("Success login of an existing user")
                    completionHandler(user)
                } else {
                    let u = User(id: auth!.user.uid)
                    u.name = "Alice"
                    u.save() { ref, error in
                        print("Success login of a new user")
                        completionHandler(u)
                    }
                }
            }
        }
    }
}

@objcMembers
class Book: Object {
    dynamic var name: String?
}

ViewController.swift にログインの実装を加える。

// ViewController.swift

import UIKit
import UserNotifications

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        
        let authOptions: UNAuthorizationOptions = [.alert, .badge, .sound]
        UNUserNotificationCenter.current().requestAuthorization(options: authOptions) { _, _ in
            print("push permission finished")
        }
      
        // ここだけ追加  
        User.anonymousLogin() { user in
            print(user)
        }
    }
}

この時点でXcodeで実行するとFirebase Authにてログイン済みのUserが追加され、Firestoreにも同じIDを持ったUserのデータが生成されている。認証周りとデータの書き込みがうまく行っていることが確認できる。

f:id:ngo275:20180710091956p:plain:w600

f:id:ngo275:20180710092031p:plain:w600

データの更新をトリガーにプッシュ通知を送信する

FCMトークンの扱い

まず AppDelegate.swift で受け取れるFCMトークンをUserオブジェクトと紐づける。ログイン処理が完了した後にFCMトークンをFirestoreの該当するUserに記録したいので UserDefaults を利用する。 AppDelegate.swift 内の、FCMトークンを受け取った場所で以下のように実装する。

// AppDelegate.swift

func messaging(_ messaging: Messaging, didReceiveRegistrationToken fcmToken: String) {
    // FCMトークンを記録しておく
    UserDefaults.standard.set(fcmToken, forKey: "FCM_TOKEN")
    UserDefaults.standard.synchronize()
    print("Firebase registration token: \(fcmToken)")
}

次に ViewController.swift で匿名ログインをしているが、ログイン完了時にUserオブジェクトの fcmTokenUserDefaults に記録しておいた値をセットして update をする。

// ViewController.swift

import UIKit
import UserNotifications

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        
        let authOptions: UNAuthorizationOptions = [.alert, .badge, .sound]
        UNUserNotificationCenter.current().requestAuthorization(options: authOptions) { _, _ in
            print("push permission finished")
        }
        
        User.anonymousLogin() { user in
            // 匿名ログイン完了時にfcmTokenをセットしてupdateする
            if let user = user {
                user.fcmToken = UserDefaults.standard.string(forKey: "FCM_TOKEN")
                user.update()
            }
        }
    }
}

この状態で実行すると以下の画像のようにFCMトークンがセットされているはず。

f:id:ngo275:20180710092216p:plain:w600

Firestoreに新しいデータを書き込む

まず、Firestoreにデータを書き込む。 ViewController.swift を少し変更する。

// ViewController.swift

import UIKit
import UserNotifications

class ViewController: UIViewController {

    private var user: User?
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        let authOptions: UNAuthorizationOptions = [.alert, .badge, .sound]
        UNUserNotificationCenter.current().requestAuthorization(options: authOptions) { _, _ in
            print("push permission finished")
        }
        
        User.anonymousLogin() { user in
            self.user = user
            if let user = user {
                user.fcmToken = UserDefaults.standard.string(forKey: "FCM_TOKEN")
                user.update()
            }
        }
    
        // ここを追加する
        setButton()
    }
    
    private func setButton() {
        let button = UIButton(frame: CGRect(x: 0, y: 50, width: view.frame.width, height: view.frame.height / 6))
        button.backgroundColor = .black
        button.setTitle("Add Book", for: .normal)
        button.setTitleColor(.white, for: .normal)
        button.addTarget(self, action: #selector(addBook), for: .touchUpInside)
        view.addSubview(button)
    }
    
    @objc private func addBook(_ sender: UIButton){
        let book = Book()
        book.name = "Alice in Wonderland"
        user?.books.insert(book)
        user?.update()
    }
}

Xcodeで起動すると以下のようにボタンがセットされている。これをタップすると、FirestoreのUserの中にBookが記録される。

f:id:ngo275:20180710212857j:plain:w300

Firebaseのコンソールで確認すると以下の画像の通り。

f:id:ngo275:20180710213038p:plain:w600

Cloud Functionsを使ってFirestoreの更新を監視する

サーバレスで実行されるCloud Functionsを実装していく。これはAWSのLambdaに相当する。GCPのコンソール側から作成することもできるが、Firebaseのコンソールから進めていくほうがずっと楽である。

f:id:ngo275:20180710213330p:plain:w500

以下のコマンドでFirebaseのCLIをインストールする。

$ npm install -g firebase-tools

続いて、Firestoreの更新があった時に、それをトリガーに実行されるコードを書いていく。Xcodeのプロジェクトとは異なるディレクトリに移動して、 firebase init を実行する。 Functions をスペースで選択し、次へ進む。

f:id:ngo275:20180710213759p:plain:w500

いくつか質問が聞かれるがここでは以下のように進めた。

=== Project Setup

? Select a default Firebase project for this directory: FirebaseSample (fir-sample-94ad3)

=== Functions Setup

? What language would you like to use to write Cloud Functions? JavaScript
? Do you want to use ESLint to catch probable bugs and enforce style? No
✔  Wrote functions/package.json
✔  Wrote functions/index.js
? Do you want to install dependencies with npm now? Yes 

✔  Firebase initialization complete!

functions ディレクトリの中にある index.js を編集していく。

// functions/index.js

const functions = require('firebase-functions')
const admin = require('firebase-admin')
admin.initializeApp()
const firestore = admin.firestore()

const pushMessage = (fcmToken, bookName) => ({
  notification: {
    title: '保存が完了しました',
    body: `「${bookName}」の保存が完了しました🙌`,
  },
  data: {
    hoge: 'fuga', // 任意のデータを送れる
  },
  token: fcmToken,
})

exports.saveBook = functions.firestore
  .document('version/1/user/{userID}/books/{bookID}')
  .onCreate((snapshot, context) => {
    // 以下のようにすればbookの中身が取れる.
    const book = snapshot.data()
    // 以下のようにすればuserIDやbookIDが取れる.
    const userID = context.params.userID
    const userRef = firestore.doc(`version/1/user/${userID}`)
    userRef.get().then((user) => {
      const userData = user.data()
      admin.messaging().send(pushMessage(userData.fcmToken, book.name))
        .then((response) => {
          console.log('Successfully sent message:', response)
        })
        .catch((e) => {
          console.log('Error sending message:', e)
        })
    }).catch((e) => console.log(e))
  })

実装したら、 firebase deploy をターミナル側で実行してデプロイする。

以下のようにデプロイされているのが確認できればOK。

f:id:ngo275:20180710215521p:plain:w600

最後にiPhone実機でAdd Bookボタンをタップすると、数秒後にプッシュ通知が届く!!

f:id:ngo275:20180710225658j:plain:w300

うまくプッシュ通知が届かない時は、FirebaseのコンソールのFunctionsのタブでログを確認しよう。

f:id:ngo275:20180710230753p:plain:w600

ついでに保存されているBookの一覧をTableViewで表示してみる。

せっかくなので、Firestoreに保存されているBookの一覧を表示してみる。 ViewController.swiftUITableView を追加して、FirestoreのUser/Booksの監視を行うように実装する。

// ViewController.swift

import UIKit
import UserNotifications
import Pring

class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {

    private var user: User?
    private var dataSource: DataSource<Book>? // TableViewに渡すDataSource
    private var tableView: UITableView!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        let authOptions: UNAuthorizationOptions = [.alert, .badge, .sound]
        UNUserNotificationCenter.current().requestAuthorization(options: authOptions) { _, _ in
            print("push permission finished")
        }
        
        setButton()
        setTableView()
        
        User.anonymousLogin() { [weak self] user in
            self?.user = user
            if let user = user {
                user.fcmToken = UserDefaults.standard.string(forKey: "FCM_TOKEN")
                user.update()
                
                let options = Options()
                let sortDescriptor = NSSortDescriptor(key: "createdAt", ascending: false)
                options.sortDescirptors = [sortDescriptor]

                // 以下のように書くと、Firestoreの更新があるとTableViewの更新もかけるようになる.
                self?.dataSource = user.books.order(by: \Book.createdAt).dataSource(options: options)
                    .on() { (snapshot, changes) in
                        guard let tableView = self?.tableView else { return }
                        switch changes {
                        case .initial:
                            tableView.reloadData()
                        case .update(let deletions, let insertions, let modifications):
                            tableView.beginUpdates()
                            tableView.insertRows(at: insertions.map { IndexPath(row: $0, section: 0) }, with: .automatic)
                            tableView.deleteRows(at: deletions.map { IndexPath(row: $0, section: 0) }, with: .automatic)
                            tableView.reloadRows(at: modifications.map { IndexPath(row: $0, section: 0) }, with: .automatic)
                            tableView.endUpdates()
                        case .error(let error):
                            print(error)
                        }
                    }.listen()
            }
        }
    }
    
    private func setButton() {
        let button = UIButton(frame: CGRect(x: 0, y: 50, width: view.frame.width, height: view.frame.height / 6))
        button.backgroundColor = .black
        button.setTitle("Add Book", for: .normal)
        button.setTitleColor(.white, for: .normal)
        button.addTarget(self, action: #selector(addBook), for: .touchUpInside)
        view.addSubview(button)
    }
    
    @objc private func addBook(_ sender: UIButton){
        let book = Book()
        book.name = "Alice in Wonderland"
        user?.books.insert(book)
        user?.update()
    }
    
    private func setTableView() {
        let f = view.frame
        // Add Bookボタンの下に配置する
        tableView = UITableView(frame: CGRect(x: 0, y: 50 + f.height / 6, width: f.width, height: f.height * 5 / 6 - 50), style: .plain)
        tableView.delegate = self
        tableView.dataSource = self
        tableView.tableFooterView = UIView()
        tableView.register(UITableViewCell.self, forCellReuseIdentifier: "BookCell")
        view.addSubview(tableView)
    }
    
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return dataSource?.count ?? 0
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "BookCell", for: indexPath)
        cell.textLabel?.text = dataSource?[indexPath.item].name
        return cell
    }
}

完成したアプリがこちら

f:id:ngo275:20180711000218g:plain:w300

FirebaseSampleのコードはこちら

github.com

FirebaseSampleFunctionsのコードはこちら

github.com

Firebaseでサーバレスアプリをサクッと作ってみる① ~Cloud MessagingでPush通知実装~

Firebaseを使ってアプリを作る

イメージはこんな感じ。二時間もあれば画像アップロードもこれに加えて画像のアップロードも実装できる。

f:id:ngo275:20180711001107g:plain:w300

Firebaseがサクッとアプリを作るのにめちゃくちゃ便利なので、その手順を書いていきたい。ここで実装する機能としては、匿名ログイン(ログイン・サインアップをさせずに認証情報を与える)、DB(Firestore)に書き込み・読み取り、DBの書き込みに応じてプッシュ通知を飛ばす、のを想定する。こういった機能を実装しようとすると一日で1人でやるにはキツイが、Firebaseを利用するとサクッと数時間もあればできてしまう。たしかに悪意のあるユーザからの書き込みをどうケアするのか(基本的にクライアントからの情報は信用してはいけない)といったことは別途考える必要はあるが、プロトタイピングするにはモッテコイなサービスだと思う。

以下、利用するサービスや構成である。

長くなりそうなので、本記事では、プロジェクトを新規作成してプッシュ通知を実機に送るところまで実装していく。Firebaseを利用したプッシュ通知の実装の参考にもなればいいなという気持ちもある。

1. プロジェクトのセットアップ

Xcodeで新規プロジェクトの作成をする。

f:id:ngo275:20180709085757p:plain:w500

次にここでFirebaseのプロジェクトを作成する。

f:id:ngo275:20180709090223p:plain:w400

続いて、XcodeにFirebaseをセットアップしていく。

f:id:ngo275:20180709090431p:plain:w500

BundleIDやアプリのニックネームを入力して次に進むと、 GoogleService-Info.plist が発行されるので、それをXcodeドラッグアンドドロップして突っ込む。

f:id:ngo275:20180709090957p:plain:w400

最後に、CocoaPodsを使ってFirebaseをプロジェクトにインストールする。

$ cd /project // 自分のproject rootに行く
$ pod init
$ vim Podfile // # Pods for FirebaseSampleの下の行に pod 'Firebase/Core' を付け足す
$ pod install

pod install が完了すると FirebaseSample.xcworkspace ができているのでそれをダブルクリックしてXcodeを開く。 AppDelegate.swift を開いて以下のように import FirebaseFirebaseApp.configure() の2行を追加する。

import UIKit
import Firebase

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {

    var window: UIWindow?

    func application(_ application: didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
        FirebaseApp.configure()
        return true
    }
}

Xcodeで実行すると以下の画面になり、Firebaseのセットアップが完了する。

f:id:ngo275:20180709092225p:plain:w500

2. プッシュ通知の設定をしていく

プッシュ通知の証明書には.p12ではなく.p8を使う。 .p8はHTTP2に対応しており、 .p12ではあった有効期限もなく、.p12ファイルの作成よりもずっと簡単 だ。これからは.p12ではなく.p8を利用する方が良さそうでFirebaseも.p8を推奨している。ただし、AWSAmazon SNSやツールによってはまだ.p8に対応していないようなので注意が必要そう。

Xcodeに戻って、Capabilitiesで Push NotificationsBackground Modes をONにして Remote Notifications にチェックを入れておく。

f:id:ngo275:20180709092608p:plain:w500

続いて、AppleDev CenterでApp IDsを開くと、先ほど作成したプロジェクトが XC hoge fuga FirebaseSample という名前で追加されているので詳細を開く。すると、Push Notificationsの状態がConfigurableになっているのでEditを選択する。DevelopmentとDistribution両方の証明書を作成していくが、そのためにはCSRファイルが必要になるので、まずそれの作成をする(と言ってもすごく簡単)。一旦、以下の画面のまま、キーチェーンアクセスを開く。

f:id:ngo275:20180709093913p:plain:w500

キーチェーンアクセスを開き、 キーチェーンアクセス証明書アシスタント認証局に証明書を要求 をクリックする(画像は英語の設定ですが同じです)。

f:id:ngo275:20180709111723p:plain:w500

ユーザーのメールアドレス を入力し、 通称 はそのまま、 CAのメールアドレス は空欄のままで良い。 要求の処理ディスクに保存 を選択し 鍵ペア情報を設定 にチェックを入れ、 続ける をクリックする。

f:id:ngo275:20180709111856p:plain:w500

鍵の情報が以下のように表示されるが特にいじらずそのまま進めるとCSRファイルがダウンロードできる。

f:id:ngo275:20180709111958p:plain:w500

再び、Dev Centerに戻って、 Create Certificate をクリックして、作成したばかりのCSRファイルをアップロードする。

f:id:ngo275:20180709112412p:plain:w500

すると、.cerファイルが生成されるのでダウンロードし、クリックする。クリックしても何も起こらないように見えるがキーチェーンアクセスにセットされるので問題ない。DevelopmentとDistribution両方とも同じCSRファイルで設定する。Dev CenterでApp IDsの当該プロジェクトを確認して以下のように、Push Notificationsが緑になっていればOK。

f:id:ngo275:20180709113108p:plain:w500

Dev CenterのKeysという欄から.p8ファイルを作成する。Keysを開いて右上の+をクリックする。画像の方に入力して次へ進む。

f:id:ngo275:20180709115742p:plain:w500

下の画像のようになれば成功で、.p8ファイルをダウンロードし、Key IDも記録しておく。.p8ファイルはここでしかダウンロードできないので紛失しないように注意しなければならない。

f:id:ngo275:20180709115927p:plain:w500

次に、Firebaseのコンソールを開いてプッシュの証明書をセットしていく。まず、設定のクラウドメッセージングを開く。

f:id:ngo275:20180709120746p:plain:w600

以下の画像にあるAPNs認証キーが.p8ファイルのことである。.p8ファイルを生成した時に発行されたKey IDとMembershipのページのTeamIDを入力する。

f:id:ngo275:20180709141840p:plain:w500

これでプッシュ通知まわりの設定は完了したので、あとはコードを書いていく。といっても書くのは主に AppDelegate.swift くらいある。

iOSプッシュ通知の仕組みについては以下が参考になる。

qiita.com

3. プッシュ通知の実装をしていく

Podfilepod 'Firebase/Messaging' を追加して、 pod install を実行する。 AppDelegate.swift を以下のように実装する。この中で各デバイスのFCMトークンを取得できる。このトークンに向かってFirebaseでプッシュ通知を送ることができるので、後々、Firestoreの書き込みに応じて任意のユーザにプッシュ通知を送る場合はこのFCMトークンが必要になってくる。ここでは一旦printするだけにしている。

import UIKit
import Firebase
import FirebaseMessaging
import UserNotifications

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {

    var window: UIWindow?

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
        
        FirebaseApp.configure()
        
        // 以下を追加する
        application.registerForRemoteNotifications()
        Messaging.messaging().delegate = self
        UNUserNotificationCenter.current().delegate = self
        
        return true
    }
}

// MARK: - UNUserNotificationCenterDelegate
extension AppDelegate : UNUserNotificationCenterDelegate {
    
    // 通知を受け取った時に(開く前に)呼ばれるメソッド
    func userNotificationCenter(_ center: UNUserNotificationCenter,
                                willPresent notification: UNNotification,
                                withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
        let userInfo = notification.request.content.userInfo
        
        // for analytics
        // Messaging.messaging().appDidReceiveMessage(userInfo)
        
        if let messageID = userInfo["gcm.message_id"] {
            print("Message ID: \(messageID)")
        }
        
        completionHandler([.alert])
    }
    
    // 通知を開いた時に呼ばれるメソッド
    func userNotificationCenter(_ center: UNUserNotificationCenter,
                                didReceive response: UNNotificationResponse,
                                withCompletionHandler completionHandler: @escaping () -> Void) {
        let userInfo = response.notification.request.content.userInfo
        
        if let messageID = userInfo["gcm.message_id"] {
            print("Message ID: \(messageID)")
        }
        
        completionHandler()
    }
}

// MARK: - MessagingDelegate
extension AppDelegate: MessagingDelegate {
    func messaging(_ messaging: Messaging, didReceiveRegistrationToken fcmToken: String) {
        print("Firebase registration token: \(fcmToken)")
    }
    
    func messaging(_ messaging: Messaging, didReceive remoteMessage: MessagingRemoteMessage) {
        print("Received data message: \(remoteMessage.appData)")
    }
}

続いて、 ViewController.swift にプッシュ通知の許可ダイアログを表示するように実装する。コードは以下のようになる。

import UIKit
import UserNotifications

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        
        // プッシュ通知の許諾ダイアログを出したいタイミングで呼んであげる. 必ずしもここじゃなくても良い
        let authOptions: UNAuthorizationOptions = [.alert, .badge, .sound]
        UNUserNotificationCenter.current().requestAuthorization(options: authOptions) { _, _ in
            print("push permission finished")
        }
    }
}

実行すると画像のようにプッシュ通知許可のダイアログが表示される。

f:id:ngo275:20180709154902p:plain:w300

ここまで来たので試しに取得したFCMトークンに対してプッシュ通知を送ってみる。Xcodeデバッグで吐かれたFCMトークンをコピーして、以下の画像のようにFirebaseのコンソールのCloud Messagingの項目からプッシュ通知を送信してみる。

f:id:ngo275:20180709155622p:plain:w500

ちゃんと届いたことが確認できた。

f:id:ngo275:20180709213441j:plain:w300

ソースコードはここにあります。

github.com

つづく。次は、Firestoreにデータを書き込んでそれをトリガーにしてプッシュ通知が届くように実装していく。

diary.shuichi.tech

Firebase Meetup #4に行ってきました

firebase-community.connpass.com

Firebase Meetup #4に行ってきた。BLOG枠での参加。なかなか盛り上がっていた。

他のBLOG枠のsu-さんの記事はこちら。

tech-blog.sgr-ksmt.org

Firestoreのセキュリティルールについて (Web) @k2wanko

speakerdeck.com

Firebase利用企業の62%がDBのセキュリティルールにミスがあり機密情報が公開されている。その中でセキュリティルール周りの知見の共有だった。

Realtime Database(以下RTDB)、Firestore、Storageでセキュリティルールという概念が出てくる。

  • RTDB
    • ゲームやチャットに向いてる
    • jsonでセキュリティルールを書いていく
    • 条件式を書いていくこともできる
    • 上層でマッチすると、下層で上書きしても反映されない。上の階層のほうがoverrideしてしまう
    • これらはjsonやらないといけない
    • コメントかけない
    • 関数がない
    • 型がない
    • ハイライトがない
    • 落ちていることがちょいちょいある

みたいな雑感。

Boltを使うと、RTDB用のjsonを吐き出してくれるし、TypeScriptのような型定義で便利。ただ、完全に信じるのは危険かも...。

github.com

  • Cloud Strorage

    • 画像とか動画とか大きめのデータ用
    • jsonではなく独自言語でかける
    • GCSのアクセス制御と同期しない
    • resource.data.visibility == 'publicだとread: if flase;でもURL経由で読み取れてしまう
    • ACLをちゃんと見よう
  • Firestore

    • RTDBの次世代のドキュメント志向のDB
    • backendはspannerなので安定性はある
    • jsonではなく独自言語でかける
    • セキュリティルールとindexが別
    • IAMの設定もできる
    • ワイルドカードを除いて、下階層に権限は影響していかない
    • get()やexists()は呼び出し回数に制限やreadのコストがかかる
    • まだベータ版だが、スケールアウトがまだ遅いためで、もう破壊的なアップデートはないと言われているらしい
  • protobug-rules-gen

    • protocol bufferの定義から型検査をしてくれる。セキュリティルールを生成するプラグイン

開発時はどうしても後回しにしてしまう部分をしっかり説明されていて、非常に参考になる内容だった。

Firebaseを使ってプッシュ通知基盤を作ったときにハマったところ、よかったところ y.danno

speakerdeck.com

アプリのリニューアル検討、Parseが2017/1/28に終了等の理由によりプッシュ通知基盤をFirebaseに移行した時のお話。

もともとはFirebase Notificationsを利用していたそうだが、送り先はifとかで決めるところがマーケティングチームがやるには厳しい、とかマニュアルではきついといった理由もあり、Firebase Cloud Messaging + αでプッシュ基盤を作成した。

Firebase Notificationsで Topic or FCM?? 問題については遅延なく送信し、届いたかどうかを取得するためにはFCMでやる必要があったそう。FCMトークンの管理そのものをどうするか、という問題もあり苦労したとのこと。Firestoreでお気に入りしたかどうかの情報を保持してそれをもとにプッシュの送信先を決めるようにしている。

プッシュ送信にはAppEngineを利用し、20万件ずつ送ったがいい感じにスケールアウトしてくれたのも良かったということだった。

今後はWeb Pushやリコメンド、緊急速報、ライブ動画のコメントあたりをFirebaseを使って解決していきたいという話だった。

Growth product @1amageek

なんで今からFirebaseなの、というテーマでの発表。

Internet普及率がここ20年強で9.2%から83.5%になっており、その間、AWSGCPのAppengineなどは、Scaleを支えるサービスを作ってきた。今後は新しいサービスを作る時代だ、ということ。

プロダクトとはコンテンツ であり、コンテンツのIN/OUTの最適化がうまくできているかどうかが重要になってくる。エンジニアリングとマーケティングはそこのIN/OUTのPDCAの高速化が求められる。

Firebaseの開発速度は破壊的でそれをライブコーディングで実演された。

インスタみたいに画像をライブラリから選択してアップロードするDEMOアプリを見せてくれたが、Xcodeソースコードを開くと、おもむろに利用しているViewControllerとかStoryboardを削除しはじめる @1amageek さん。そして、5分たらずでFileのアップロードやデータの取得、その描画をライブで実装していた。とにかく早い。AWSや既存のREST APIを作るスタイルであれば一人で半日はかかると思われるコードもものの数分だった。

github.com

Firebaseは作っては壊す、に非常に向いている とのこと。

Quickstart-android/MLKitについて @yamacraft

speakerdeck.com

MLKitを動かすために公式サンプルが行っていることの解説をされていた。

  • カメラの制御
  • onPreviewFrameでプレビュー画面のBufferをFirebaseVisionFaceDetectorに流し込み
  • その結果をもとにプレビュー画面に結果をオーバーレイ

MLKitの部分は簡単だが、サンプルコードのほとんどがカメラの制御になっている...!!カメラのデータをVision APIに送り続けるとか、受け取ったデータをプレビューに表示し続けるのが結構手強いらしい。

Firebase Authenticationで色々使ってみた @teyosh

f:id:ngo275:20180703002912p:plain:w500

  • ユーザ管理は以下のようにプロパティがある

    • uid
    • display name
    • email
    • phoneNumber
    • photoURL
  • 対応プロバイダ・方式は以下の通り。

loginしたuserとcredentialのヒモ付が簡単で user.linkAndRetrieveData(with: credential) でいける。メールの更新等も user.reauthenticateAndRetrieveData(with: credential) でいける。

Firestoreをもっと手軽に使えるfirestore-simpleを作った @Kesin11

speakerdeck.com

APIが言語によらずだいたい同じだが、汎用化してライブラリ作ったほうがもっと気持ちよくかけるよね、という動機でライブラリを作られたそう。

以下のようなツラミが開発していてあったそう。

  • 素のFirestoreは毎回コレクションへのパスを書くのはめんどくさいので、自分でコレクションごとのモデルを作るか
  • FirestoreではQuerySnapshotで返ってきてArrayではないからメンドくさいから、いい感じにArrayで扱えるようにするか

Cloud Functionsとかはネイティブアプリエンジニアでも利用することはあるので、ぜひ触ってみたいと思った。

まとめ

Firebaseの盛り上がりを感じた。結構iOSAndroidのクライアントのエンジニアが多かった印象。Firestoreを触っている人が多く、いかに大きなインパクトを与えているのかを再認識した。

先週末にFirestore・Pringを使った記事も上げたのでぜひ参考に!

diary.shuichi.tech

次回はLTする予定なのでよろしくお願いします!

firebase-community.connpass.com

Firebaseでサーバレスなバックエンド構成のiOS開発をする ~Firestore・Pring~

f:id:ngo275:20180701153549p:plain

Firebaseを使ってサーバレスなiOS開発をしていたのだが、うまく使えば非常に強力な武器である一方、Firebase独特のデータ設計周りのBest Practiceがわからず苦戦したので、Firebase、特にFirestoreでのデータの扱いを中心に書いていく。

FirestoreはFirebase Realtime Databaseの後継のイメージで、データをどう扱うのかという議論はしばしば取り上げられている(参考)。

利用するサービス

以下のライブラリやサービスを使うと完全にサーバレスなネイティブアプリ開発ができる。

ここではPringを用いてFirestoreのデータを取得している。コードの書き方はFirebaseのドキュメントのそれとは異なり、Pringに依存したものになっていることに注意。

ちなみにFirestoreはベータだが、スケールアウト時の速度が物足りないから、だそうで、データのバックアップやロールバックはないもののそれ以外は特に問題がないそうだ。なので破壊的なアップデートはないとのこと。

具体的な実装を見ていく

Userが複数のBookを所有している例(User has many Book)を考える。

リレーションシップを持たせるには NestedCollectionReferenceCollection がある。前者は同じツリーの中にネストしてデータを保存していく。 User -> Book -> Highlight のように段々とネストが深くなっていく(Firestoreではネストが深くなればなるほど親のデータの読み込みが重くなる、という問題はない)。ReferenceCollectionはテーブル自体をフラットにして、お互いのテーブルのリレーションを参照でつないでいくものだ。Railsでいうところの user.books.where(name: 'test') のような書き方を実現するにはNestedCollectionを使うことになる。単純なリレーションシップを実現するならNestedCollectionで良いのではないかと思う。ので、まずUserとBookの関係はNestedCollectionで進めていく。

NestedCollectionを利用してみる

モデルを定義する

以下のようにclassを定義する。

import Pring

@objcMembers
class User: Object {
    dynamic var name: String?
    dynamic var thumbnail: File?
    dynamic var books: NestedCollection<Book> = []
}

@objcMembers
class Book: Object {
    dynamic var thumbnail: File?
    dynamic var name: String?
}

データをFirestoreに書き込む

以下のようにしてFirebaseのコンソールを開くと、Userの中にBooksが記録されているのが確認できるはずだ。サムネイル画像のアップロードもこれだけで完了するというお手軽さ...!!確認するには書き込み権限まわりをゆるくしておいたほうが良いかもしれない。Firebase Anonymous Loginを利用すれば認証周りも一瞬で実装できる。

let user = User()
user.name = "Alice"
user.thumbnail = File(data: UIImageJPEGRepresentation(someUIImageA, 1), mimeType: .jpeg)

let book = Book()
book.name = "Alice in Wonderland"
book.thumbnail = File(data: UIImageJPEGRepresentation(someUIImageB, 1), mimeType: .jpeg)

user.books.insert(book)
user.save()

UserとBookをFirestoreから取得する方法いろいろ

// userをid指定で取得する
User.get(id) { user, error in
    print(user) // 存在すればデータが出力される
    print(user.books) // これだけでは必ずnilが出力される
}

// あるuserが持っているbookをid指定で取得する
user.books.get(bookID) { book, error in  
    print(book)
}

// userの一覧を取得する
var dataSource: DataSource<User>?
dataSouce = User.order(by: \User.createdAt).dataSource()
    .onCompleted() { (snapshot, users) in 
        // 各userは例えばusers.first?.booksとしてもデータはこれだけだと入っていない
        print(users)
    }.listen()

// 各userのbooks付きでuserの一覧を取得する
dataSouce = User.order(by: \User.createdAt).dataSource()
    .on(parse: { (_, user, done) in
        // 意図的にuserと関連付けされているbooksを持ってくるように指示しないといけない
        user.books.get() { (snapshot, books) in
            done(user)
        }
    })
    .onCompleted() { (snapshot, users) in 
        // on(parse:)をはさんでbooksを取得すると、ここでusersの中にある各userはbooksを参照できる
        print(users)
    }.listen()

// あるuserが持っているbookの一覧を取得する
var bookDataSource: DataSource<Book>?
bookDataSouce = user.books.order(by: \Book.createdAt).dataSource()
    .onCompleted() { (snapshot, books) in 
        print(books)
    }.listen()

// あるuserが持っている特定のbookを取得する
bookDataSouce = user.books.where(\Book.name, isEqualTo: "Alice in Wonderland").dataSource()
    .onCompleted() { (snapshot, books) in 
        print(books)
    }.listen()

// あるuserが持っているbookの一覧を更新したもの順に取得する
// user.books.order(by: \Book.createdAt)の設定とDataSourceに渡しているソートは別で、DataSourceの並びはoptionsを渡す必要がある
var bookDataSource: DataSource<Book>?
let options: Options = Options()
let sortDescriptor: NSSortDescriptor = NSSortDescriptor(key: "updatedAt", ascending: false)
options.sortDescirptors = [sortDescriptor]
bookDataSouce = user.books.order(by: \Book.createdAt).dataSource(options: options)
    .onCompleted() { (snapshot, books) in 
        print(books)
    }.listen()

このDataSourceはUITableViewやUICollectionViewのDataSourceとして利用することができる。また、 dataSource: DataSource<User>[User] (Userの配列)だと思って操作することも可能である。UITableViewやUICollectionViewと合わせて利用する場合は、PringのREADMEにあるように記述すればデータの更新と同時にUIの更新が走っていい感じになる。

NestedCollectionの制約

Bookのidがわかってもいきなりbookの取得ができず、親(User)のidが必要になる。具体的には以下の通り。

// bookIDがわかっていてもuserIDがわかっていないと取り出せない
book.get(bookID) { book, error in
    print(book)  // nilが返ってくる
}

// userIDをどこかで取得してから以下のようにbookを取り出す必要がある
let user = User(id: id, value: [:])
user.books.get(bookID) { book, error in
    // print(book)
}

さらにもっと大きな制約がある。

ここではUser -> BookとたどっていたのでNestedCollectionで問題なかったが、もしCategory -> Bookという経路でもBookを取得したくなると、途端に問題が複雑になる。NestedCollectionだと、Userのidが必須になるためである。したがって、User以外の経路でBookにアクセスすること可能性が出てきうるなら、最初からUserの直下に入れずにReferenceCollectionを利用しておくのが良さそうだ。実際に利用してみた感想としては、密接に関係していないところはReferenceCollectionで管理した方が安全だが、確実にこのルートからしか取得しないよね、というものはNestedCollectionで良いと思う。

NestedCollectionを利用しつつ、Userと同じ階層にもう一つBookのノードを作成する(すなわちUserの直下とUserと同じ階層にBookデータを保存する)ことも可能ではあるが、データの実体が複数存在させるのはメンテナンスコストを考えると避けたほうが良い。暗号通貨のウォレットを作っているGincoではデータの冗長性は割り切っており、Cloud Functionsでデータの整合性を取るように処理しているらしい。

ReferenceCollectionを利用してみる

モデルを定義する

前回に加えてCategoryを定義しておく。

import Pring
import FirebaseFirestore

@objcMembers
class User: Object {
    dynamic var name: String?
    dynamic var thumbnail: File?
    dynamic var books: ReferenceCollection<Book> = []
}

@objcMembers
class Book: Object {
    dynamic var thumbnail: File?
    dynamic var name: String?
}

@objcMembers
class Category: Object {
    dynamic var thumbnail: File?
    dynamic var name: String?
    dynamic var books: ReferenceCollection<Book> = []
}

データをFirestoreに書き込む

以下のようにしてFirebaseのコンソールを開くと、User、Book、Categoryがフラットに記録されている。

let user = User()
user.name = "Alice"
user.thumbnail = File(data: UIImageJPEGRepresentation(someUIImageA, 1), mimeType: .jpeg)

let book = Book()
book.name = "Alice in Wonderland"
book.thumbnail = File(data: UIImageJPEGRepresentation(someUIImageB, 1), mimeType: .jpeg)

let category = Category()
category.name = "Fairy tale"
category.thumbnail = File(data: UIImageJPEGRepresentation(someUIImageC, 1), mimeType: .jpeg)

user.books.insert(book)
category.books.insert(book)

// batchでUser、Category、Bookを保存する(Userは保存したけどBookは保存できていない、ということが起こらない)
let batch = Firestore.firestore().batch()
user.pack(.save, batch: batch)
category.pack(.save, batch: batch)
book.save(batch)

User・Book・CategoryをFirestoreから取得する方法いろいろ

// userをid指定で取得する
User.get(id) { user, error in
    print(user) // 存在すればデータが出力される
    print(user.books) // これだけでは必ずnilが出力される
}

// bookをid指定で取得する
Book.get(id) { book, error in
    print(book) // 存在すればデータが出力される
}

// categoryをid指定で取得する
Category.get(id) { category, error in
    print(category) // 存在すればデータが出力される
    print(category.books) // これだけでは必ずnilが出力される
}

// あるuserが持っているbookの一覧を取得する
// これに加えてbook.nameでフィルターをかけることはできない
var bookDataSource: DataSource<Book>?
bookDataSouce = user.books.order(by: \Book.createdAt).dataSource()
    .onCompleted() { (snapshot, books) in 
        print(books)
    }.listen()

// あるcategoryのbookの一覧を取得する
bookDataSouce = category.books.order(by: \Book.createdAt).dataSource()
    .onCompleted() { (snapshot, books) in 
        print(books)
    }.listen()

ReferenceCollectionだとwhereを使ったフィルターが利用できないというデメリットがある。

まとめ

  • NestedCollectionとReferenceCollectionの使い分けは、モデル間の関係性の度合いによる。依存関係があるなら前者を採用して良さそう
  • 今までのRDBと同じ感覚で利用するとかえって辛い。割り切って見た目に依存する形で設計すると良さそう
  • Fileのアップロード・認証周りがめちゃくちゃ簡単

高齢化社会にどう立ち向かえばいいのか

重荷扱いされる高齢者たち

高齢化問題。

日本における高齢者(65歳以上)の割合はおよそ全体の3割となり、このままでは非常にまずいという議論をよく聞く。社会保障問題や介護問題、労働力の低下、医療費問題、などと高齢化にまつわる問題がたくさんあるようだ。たしかにそれはもっともらしく聞こえるが、 そもそも高齢者 = 重荷という捉え方は間違っている と思っている。中にはめちゃくちゃ元気な人も多くいるにもかかわらず、高齢者は社会によって支えられている存在、とひとくくりにするのは乱暴すぎる。むしろ、医療が発展し人生100年時代と言われる中で、みんながみんな65歳を過ぎたからといって重荷になるはずがない。

シニア層はやることが極端に少ない

今の若者は普段何をしているだろうか。勉強、運動、テレビ、ゲーム、SNSYouTube等々。スマホやパソコンに慣れ親しんでいると娯楽は無限にある。アプリやWebサービスを提供している会社の多くはこういった若者をターゲットにしており、常に新しいサービスが生まれてきている。ただでさえ、LINE、InstagramYouTubeNetflixなどの超人気サービスに多くの時間を費やしているところに、自分たちのアプリを使ってもらうべくいろいろな施策を打っているのだ。

一方で、シニア層(以下、高齢者のことをこう呼ぶことにする)は定年退職を迎えたあと何をしているのだろうか。テレビ、勉強、読書、旅行、ハイキング、ゴルフ、運動。こういったことがパッと思いつくが、スマホやパソコンをずっといじっているところは想像できない。仕事を続けたくても次第に追いやられていくし、自分たちにフィットする新しい娯楽・エンタメがバンバン生まれてくるわけでもない。やることがアップデートされていないのだ。極端な話、シニア層が重荷として捉えられている問題は若者とシニア層との対立の現れであって、その 対立の原因は、単純にシニア層が暇だからなのではないか と思う。何かに熱中していると、些細なことでいちいち他人に口出ししないはずだ。逆にすることがないとちょっとしたことでついつい難癖をつけてしまう。

なぜシニア層は暇なのか

iPhoneが発売されて10年になるが、今やスマホを持っていない若者はいないはずだ。彼らは一日に何時間もの時間をゲームやSNSYouTube等インターネットサービスに費やしている。これらのアプリやWebサービスを作るにあたって元手となる資金はほとんど0である。なおかつそういったサービスを作るためのオフィスのような場所も必要ない。さらには、自分の労働力だけを元手に、うまく行けば億単位で売却できる事業が作れる可能性がある。

一方で、スマホやパソコンに縛られないもの、すなわち、IoTやロボットといったハードウェアの開発をしようとすると、ある程度資金を調達しなければならないし、ビジネスとしてやっていくためにはどうやって開発・生産していくのかをより綿密に考える必要がある。どこか海外で大量に生産するのか、自分たちで工場まで持つのか、いずれにせよ莫大なお金がかかる。しかも、いざマネタイズしようにもネットビジネスほど楽じゃない。

ネットサービスの開発の方がハードルが低いのは事実であり、ネットサービスを使い慣れている若者がどうしてもターゲットになりやすい。こういった理由から(もちろんこれだけはないが)、多くのIT企業はスマホやパソコンに慣れ親しんだ若者向けのサービスを提供してしまいがちだ。時間もお金も、若者より余裕があるはずのシニア層向けにサービスがあまり作られていない現状なのだ。たしかにシニア層向けのサービスも作られてはいるが、エンタメというよりは、シニア層ならではの問題を解決するためのサービスが多い。シニア層が熱中しているソシャゲーとか聞いたことがない。

シニア層がテクノロジーに慣れてしまえばいい

テクノロジーが著しく発展しているにもかかわらず、上述したようにシニア層が熱狂するようなエンタメが生まれにくい環境がある。これを解決するにはシニア層がテクノロジーに親しみを持つことが必要である。シニア層がテクノロジーの楽しさ、可能性を体感し、あわよくば彼らが自分たち向けにエンタメを作り出せれば一番理想だ。若宮正子さんという方は、80歳を過ぎてからインターネットに触れてその可能性を感じたという。ところが、 世の中のアプリ・ゲームは若者向けばかりで退屈だと思い、なんと自分でアプリを開発したのだ。

www.buzzfeed.com

こういった現象が自然と起きていく仕組み作りをすることに価値があると思う。

まとめ

高齢化問題をどう解決するのか、という議論については、そもそもシニア層は退屈しているのが問題なのであって、大勢の高齢者をどう支えるのか、と考えてしまう前提そのものが間違っている、と僕は思っている。彼らが熱狂するようなエンタメを提供したり、 テクノロジーに可能性を感じて自分たちでものを作る楽しみを知る仕組みを作る のが本質的な解決につながると思う。難しいのは彼らがどうすればインターネットやテクノロジーに関心を持つのか、ということで、おそらくスマホやパソコンで完結するサービスではない。慣れ親しんだインターフェイスでテクノロジーを届ける必要がありそうだ。

もちろんこれには時間がかかるし、すでに起こっているいろいろな問題を無視した考えだが、日本以外の国もいずれは高齢化社会になるのは確実なので、高齢化社会に立ち向かう術を確立すればアメリカや中国、多くの国に適用することができるはずだ。

久しぶりに実家に帰って感じたこと

久しぶりに実家へ

4月・5月は気胸を2回発症、度々入院し、そして手術をするに至った。なかなか大変な月になったが、やっと体調も落ち着いてきたので週末を使って実家に帰ることにした。

職場が六本木ということもあり、一人暮らしで、普段はかなり都会生活をしている。断っておくが、決していい暮らしをしているというわけでもなく、単純に都会に暮らしているだけだ。横浜にある実家に戻ると、そういった都会生活では普段気が付かないことを発見できた。

時間に対する感じ方の違い

特に予定もなかったので1人で近所のららぽーとに行った。日曜ということもあり信じられないくらい混んでいた。まるで渋谷のよう。当たり前だが、年齢層が六本木で見かける人と全く違い、中高生・子連れの若い家族が大多数だった。

普段の生活で見かける人(特にスタートアップの人)を振り返ると、時間に対する考え方がシビアで、働く自分が中心にいて 時間 = 最も大切なもの という価値観なんじゃないかってくらいだが、ららぽーとや実家の方で目にする人たちは、子供が中心にあって 時間 = 楽しく過ごすもの であるように映った。なので前者のほうがキビキビ行動する一方で、後者の方はまったり過ごしているようにも見えた。もちろん、「都会で生活している = 時間が超大切でせっかちな人」というわけではなく「比較すると」時間をより大切にしている、という話である。

久しぶりに実家に帰ったのにあまり変化がない

久しぶりに実家に帰ったが(といっても今年二回目)、ここ数年暮らしが全然変わっていないと気づいた。たとえば中国の深センに行くと、行くたびに街の光景が変わっているのだが、自分の家は置いてあるものや仕組みが一切替わっていない。

今まであまり気にしていなかったが、紙のカレンダーにみんなの予定が断片的に書いてあったのが印象的だった。今だったらスマホで簡単に管理・共有ができて、それが自分の中では当たり前になっていた。他にも、入院時の保険書類を記入していたが、あまりにも複雑で、わかりにくいUIなことに気になってしまったり、書類のプリントアウトをする時のプリンタのひどいUXが気になってしまった。こんなにインターネットやIoT、VRがめまぐるしく変化しているのにうちは特に変わっていなかったのだ。変わっていたところがあるとすると、みんなiPhone8になっていたことくらい。

なんで便利なものが世の中にあるのに使わないのか

カレンダーも紙で管理するよりスマホでどこでもアクセスしたほうがいいんじゃない?とか、なぜ便利が物があるのに使わないのか聞いてみた。結局、今まで慣れ親しんだものからの スイッチングコストが死ぬほど高い ことが大きな理由のようだった。LINEくらいみんなが使っていると使い始めるみたいだが、周りが使っていないとわざわざ自分から新しいものを検討をしないようだ。

スマホに慣れ親しんだ世代から考えるとすべてアプリでやったほうが楽だし、新しいアプリを使ってみるということの障壁は低いのだが、そうでない世代がいるのも事実で、そういった世代に対するアプローチがあまりできていないのでは?、と感じた。今までのツールと同じインターフェイスで利用できるが、内部では今の技術を使うのは面白いと思う。たとえば、カレンダーの話で言うと、入力は紙のままで画像認識とかを使って入力されたデータがカレンダーアプリにリアルタイムで同期されるような感じだ。そういったところから徐々にテクノロジーや新しいものに対する理解を深めてもらえばいいのかもしれない。

まとめ

とりとめもない内容になってしまったが、テクノロジーに対する感度の違いでここまで変わってくるのか、と実感した。今までにないアプリを作って一発当てる、というよりもアプリに考えを縛られないでモノづくりがしたいと思う。