アクトインディ開発者ブログ

子供とお出かけ情報「いこーよ」を運営する、アクトインディ株式会社の開発者ブログです

アプリ向けの GoogleAnalytics に替わる効果分析ツールの検討

アプリ向けの GoogleAnalytics の利用が 2019 年 10 月を持って終了する事がアナウンスされて衝撃を受けた iOS アプリエンジニアの namikata です。いこーよアプリの効果分析では、主に GoogleAnalytics のデータを元に日々の分析を行っていましたので、サービス終了は大きな打撃になりました。

アプリの効果分析では、大まかにまとめると以下のようなツールを使って今まで行っていました。

  1. 毎日ウォッチするデータ KGI, KPI( いこーよアプリの場合は口コミ投稿数や、スポットの行きたい数など )
    • スプレッドシートの GoogleAnalytics アドオンを利用して、スプレッドシートで毎日チェック
  2. 昨対や iOS と Android の効果比較など、定期的にチェックするデータ
    • Google Analytics のマイレポートの作成や、定点観測用として DataPortal(GoogleAnalytics のデータを利用) でチェック
  3. 利用している OS のバージョンや、アプリのバージョンの割合など、不定期に確認したいデータ
    • GoogleAnalytics のコンソールでチェック

Google Analytics で収集したイベントデータを中心に分析を行っていたので、サービス終了に合わせて、 Analytics のツールを刷新する必要がでてきました。リプレイスするのでもっとデータドリブンな開発ができるように、いけてる BI ツールを使おうとか、色々検討するポイントはありますが、今回の効果分析ツールのリプレイスのゴールは、ミニマムに「今までできていた分析が出来ること(低コスト(工数、予算を含む)だと嬉しい)」といった、現状維持を目指すリプレイスになりました。

現状維持を目指すリプレイスでは、最終的に、以下の形に落ち着きました。

  1. Firebase Analytics でイベント収集 + BigQuery へエクスポート
  2. Google DataPortal で BigQuery を可視化
  3. 個別で集計したい場合は BigQuery のコンソールから Query 発行

なぜ、このような形に落ち着いたのか、各種ツールの選定理由や、導入時に気をつけるポイントなどをまとめていきたいと思います。

Firebase Analytics でイベント情報を収集する

f:id:t-namikata:20190726142101p:plain

Google Analytics のコンソール画面でもお知らせが出ているように、Firebase Analytics を利用する事にしました。元々いこーよアプリでは Firebase Analytics をリリース当初から使っていた為、イベント情報はある程度 GoogleAnalytics と併用して収集してありました。新規でやる方もイベント情報の収集はほぼ Firebase Analytics 一択になるんじゃないかなと思います。無料で利用を開始できるし、生ログは BigQuery にエクスポートすれば、自由に閲覧する事ができます。

Firebase Analytics を利用する事を決めたら、すぐさま従量課金( Blaze プラン)に変更し、 Analytics のデータを BigQuery へエクスポートする設定を入れてください。Firebase の WEB コンソールでポチポチっとすれば設定できます。プランを Blaze に変更しても、今まで通り Firebase の無料枠は適用されるので、下手な使い方をしなければいきなり費用が発生しだすという事はありません。収集するログのデータが膨大な場合は BigQuery のストレージ料金が発生しますが、それも微々たるものだと思います。Firebase Analytics の WEB コンソールでの分析は、 Google Analytics のコンソールと比べると、おもちゃみたいなもので、ある条件で Filter したデータを見たい。みたいな事をやる為には、 BigQuery に検索をかけないといけないので、BigQuery のエクスポート設定をしておきましょう。

本番用の Firebase アカウントとステージング用の Firebase アカウントはそれぞれ作成する

イベント情報は Firebase Analytics で収集し、 BigQuery のログを解析する事が決まりました。 BigQuery は、ログを検索する際のデータ量に応じて課金されるので、本番と同じイベントを収集するステージング用の Firebase アカウントを作成しておきましょう。BigQuery と分析ツールを連携する際の検証として大いに役に立ちます(むしろそれがないと怖くて触れません)

iOS の場合は本番用とステージング用の plist をそれぞれ配置し AppDelegate で読み込むファイルを書き換えてあげます。

f:id:t-namikata:20190726142130p:plain

#if DEBUG
    let firebasePlistName = "GoogleService-Info-Debug"
#else
    let firebasePlistName = "GoogleService-Info"
#endif
if let firebaseOptions = FirebaseOptions(contentsOfFile: Bundle.main.path(forResource: firebasePlistName, ofType: "plist")!) {
    FirebaseApp.configure(options: firebaseOptions)
}

Firebase Analytics と BigQuery で分析できる事を把握する

GoogleAnalytics で分析できていた事を、 Firebase Analytics と BigQuery でやるとしたらどうなるでしょうか。

1. 毎日ウォッチするデータ KGI, KPI( いこーよアプリの場合は口コミ投稿数や、スポットの行きたい数など )

収集したデータにコンバージョンを付け、Firebase の WEB コンソール で確認できます。 Chrome の起動時に開くページに設定しておけば、ストレスなく日々のデイリーデータはチェックできると思います。

f:id:t-namikata:20190726142251p:plain

2. 昨対や iOS と Android の効果比較など、定期的にチェックするデータ

Firebase の WEB コンソールでやれない事はないですが、ものすごく辛い作業になります。Firebase Analytics + BigQuery だけではどうにもならない為、分析ツールの導入が必要になります。

3. 利用している OS のバージョンや、アプリのバージョンの割合など、不定期に確認したいデータ

Firebase Analytics の WEB コンソールには、Google Analytics のコンソールで行えていたような、アプリの利用バージョンが 2.0.0 以上のユーザーの割合をフィルターして検索する、といったような使い方が出来ません。BigQuery のデータと連携できる分析ツールを導入するか、 BigQuery に直接 Query を投げ結果を取得する必要があります。

今回は、不定期に見るデータだったので、必要になったら都度 BigQuery に計測用の Query を投げて計測していこうとなりました。Query を投げる頻度が多いという事は、それは定点観測したいデータという事になるので、「2」の定期的にチェックするデータに分類されるはずです。

f:id:t-namikata:20190726142347p:plain

定期的にチェックするデータを分析する為のツールを選定する

Google Analytics で行えていた、今まで通りの効果分析を行う為には、 Firebase Analytics + BigQuery では足りないといったことが分かりました。Firebase Analytics の WEB コンソールではさくっとデータを確認する事はできますが、 Google Analytics のレポートの作成や、セグメントで iOS と Android を選択して効果比較をしたり、みたいな事はできません。

↓こうゆうのはない f:id:t-namikata:20190726151347p:plain

BigQuery のデータをグラフなどで可視化する分析ツールを利用します。BigQuery のデータを分析できるのに有名なサービスとしては Tableau, Looker, Google DataPortal, Re:dash あたりが候補に上がると思いますが、いこーよアプリでは Google Analytics 時代から DataPortal の導入実績があったので、 DataPortal を利用して、定点観測を行う方向になりました。 今回のゴールが「今までできていた分析が出来ること(低コスト(工数、予算を含む)だと嬉しい)」なので、 DataPortal が一番低コストだったからです。ただ DataPotarl は表示が遅かったり(BI Engine でメモリ沢山積めば高速化しそう)、Google Analytics のようにレポートをその場で任意のキーワードで filter するみたいな事ができないので、Google Analytics のマイレポートの部分を DataPortal で補っているような使い方をしています。将来の展望としては、最近 Google に買収された Looker を使って分析の深堀りをしたいなと思っています。

↓ DataPortal で BigQuery のデータをグラフ化したもの f:id:t-namikata:20190726142446p:plain

DataPortal を使って BigQuery のデータを可視化

事前準備

BigQuery は、データを検索する際のデータ量に応じて課金が発生するので、意図せず、金額が発生してしまう事があります。なので、まずやるべき事は、上限の設定と、現在の課金状況をすぐに確認できるようにしておくのが安心安全というものです。

GCP の BigQuery の管理コンソールから、 1 日の使用量の上限を設定する

BigQuery には 1 日の使用量の上限を設定できるようになっているので、自分達のサービスに合った適切な設定をしておきましょう。 1 日 1 TB に制限すれば、毎日 Max まで利用したとしても 30 日間で 30 TB なので 30 * 5 = 150ドルぐらいの費用で頭打ちに設定することができます。

f:id:t-namikata:20190726142520p:plain

BigQuery の使用量を DataPortal でグラフ化する

DataPortal を利用した事がない方は、練習も兼ねられる良い材料です。BigQuery に発行された Query のログを BigQuery に保存して、グラフ表示します。こちらの記事が大変参考になりました。ありがとうございます。

blog.engineer.adways.net

アプリのイベントのレポートを作成する

これでやっと準備が整ったので、アプリで分析したいデータを Google DataPortal でグラフ化していきます。

さすが Google 同士のサービス。 DataPortal から接続したいデータソースで BigQuery を選ぶと、何も迷うことなくデータの連携が行え、すぐにグラフを描画する事ができるようになります。ここの所は Firebase Analytics の BigQuery 用の DataPortal のテンプレートが用意されているので、そのテンプレートを覗けば色々と使い方が見えてくるかと思います。

support.google.com

簡単に以下のような定点観測用のシートを作成できました。

f:id:t-namikata:20190726142446p:plain f:id:t-namikata:20190726143155p:plain

BigQuery BI Engine を使ってグラフの描画を高速化する

DataPortal を使えば、すぐにその遅さに気づくと思います。描画に数 10 秒待たされます。いくら費用を捻出できるかによりますが、BI Engine を使う事で、リスクを追うことなく、グラフ描画を高速化できるので、積極的に利用していきましょう。また、メモリ上にキャッシュされるみたいなので、 BigQuery の使用量の削減にも繋がるらしいです(どれぐらい削減されるのかは正直分かってないです)。今はベータ版なので利用は無料ですが、ベータが外れると課金が発生するので、発生しても問題ない金額設定にしておいた方がいいと思います(BI Engine の最低使用料金は 1 GB で 30$/月 ぐらいです)。BI Engine を有効にすると、グラフの右上に稲妻マークが表示され、BI Engine が有効に機能しているか確認する事ができます。

f:id:t-namikata:20190726143219p:plain

まとめ

アプリ向けの Google Analytics の廃止に伴う、データ分析ツールのリプレイスですが、最終的には以下になりました。

1.毎日ウォッチするデータ KGI, KPI( いこーよアプリの場合は口コミ投稿数や、スポットの行きたい数など )

Firebase の WEB コンソール f:id:t-namikata:20190726142251p:plain

2.昨対や iOS と Android の効果比較など、定期的にチェックするデータ

BigQuery + DataPortal f:id:t-namikata:20190726143155p:plain

3.利用している OS のバージョンや、アプリのバージョンの割合など、不定期に確認したいデータ

BigQuery のコンソールで Query 発行 f:id:t-namikata:20190726142347p:plain

なんとか日々の数値を継続してウォッチしていけるようになりましたが Google Analytics の時よりも、計測のしやすさや使い勝手は落ちてしまっていると感じています。 いこーよアプリチームには、専属のデータアナリストはおらず、各メンバーがそれぞれ効果の分析を行っていて、データドリブンな開発はまだまだやれていないので、今後は効果分析にも更に力を入れつつ、アプリをグロースさせて行きたいと考えています。今回のデータ分析ツールのリプレイスを機に Looker への興味が非常に強く湧いてきたので Looker を今後深堀りしていきたいなと思っています。

データドリブンな開発をしたいエンジニアを積極採用中ですので、是非、お気軽にお声がけください。

actindi.net

arm64 のみにしたら TestFlight の配布が早くなる?

iOS アプリエンジニアの namikata です。いこーよアプリでは、リリース前テストとして TestFlight を利用しています。この TestFlight ですが、とにかくアップロードしてから配布可能になるまでが遅い。アップロードが完了して、輸出コンプライアンスの設定を行い、テストが開始できるまで、体感として 40-60 分ぐらいかかっている印象があります。先日、いこーよアプリの対象バージョンを iOS11 以上に変更した為、armv7 と armv7s のサポートが不要になった為、削除し、arm64 と arm64e のみにしたのですが、 そうしたら、なんと TestFlight のテスト開始までの時間が早くなりました(多分!)

対応前(バージョン 3.8.0.0 )

f:id:t-namikata:20190208101635p:plain

対応後(バージョン 3.8.0.1)

f:id:t-namikata:20190208101626p:plain

f:id:t-namikata:20190117065030p:plain

お分かりいただけるでしょうか。 3.8.0.0 は armv7 と armv7s をまだサポートしている状態で 5:34 にアップロード完了しているのにまだ処理中となっています。 3.8.0.1 は 6:24 にアップロードして、 6:45 にはテスト可能になってました。

f:id:t-namikata:20190117065138p:plain

armv64 のみサポートしたバージョンのものは、対象となるデバイスの種類が減った事により、テスト開始までの時間が短縮されたんだと思います。素晴らしいですね。リリースサイクルに関わる部分なので、まだ対応されていない方は是非対応してみてください。

そろそろ fastlane とか使って、アプリの申請まで自動化したいなぁと思う今日この頃です。いこーよアプリチームでは、アプリの成長を加速させる為に、モバイルエンジニアを募集しています!

actindi.net

お気軽にご連絡お待ちしています!

新規開発やリニューアル時はアイデアを全て形にする意気込みで開発に臨む

actindi Advent Calendar 2018、4日目の記事を担当する、一番得意なゲームは 64 のスマブラの namikata です。ドンキーかフォックス使いです。先日は、社内で Nintendo switch のマリオカート大会が、色んな部署から 10 名ぐらい有志が集まって開催されました。同じエンジニアチームの moriyama がぶっちぎりの優勝でした。とても誇らしい気持ちになりました。いつか tech ブログで、スマブラやマリオカートの tech を紹介するのがここ最近の野望です。

今年のアクトインディアドベントカレンダー 第 1 弾の記事 の一部で honda さんが iOS アプリの UI リニューアルの件を紹介してくれたので、せっかくなので、 UI リニューアルをエンジニアの視点で振り返り、良かった点、悪かった点で 1 番印象に残っているものを書きたいと思います。

はじめに

今回の UI リニューアルは、アプリの初回表示画面をマップ表示から変更し、より普段使い出来るアプリを目指して改修したプロジェクトになります。

f:id:kou_hon:20181130135235p:plain:w200 ==========> f:id:kou_hon:20181130134850p:plain:w200

良かった点

良かった点は、今後の開発でも引き続き継続してやっていく事になります。今回のプロジェクトで一番良かった事は アイデアを全て形にする意気込みで開発に臨めた 事です。

日々の運用で行う改修は、解決したい課題が明確で、改修内容をイメージしやすいものが多いと思います。例えば、「天気が晴れか雨かで、遊びの候補は大きく変わるから、雨の時にパッと遊び場の候補を切り替えられるようにしたい」といった要望があった場合、解決したい課題は「外は雨なのに、公園とか紹介されてもノイズにしかならない」といった具体的な内容になり、課題を解決する為のアプローチとしては「雨の日で絞り込めるボタンを用意する」「現在の天気から紹介する遊び場を変える」とかいったものが出てくると思います。解決したい課題が具体的なので、改修案はある程度限られてきます。

それに対して、リニューアル規模の改修では、解決したい課題がアプリのコンセプトに近く、より抽象的なものになる事が多いと思います。今回のトップページのリニューアルの目的は、かなりざっくり言ってしまえば「近場の新しい遊び場がもっと簡単に見つかるようにする」と言ったもので(実施には、どんなターゲット層に、どのような経験をしてほしいかといった細かいストーリーがあります)、扱うテーマが大きくなると、それだけ、それを解決する為のアプローチは多くなるので、多種多様なアイデアが出てきます。

それらのアイデアは、頭の中でシミュレートして絞り込まれ、チームとの話し合いの中でさらに絞り込まれ、色々な過程をとって厳選されていき、ある程度絞り込まれると、デザインが作られます。そして作られたデザインを元にエンジニアが実装を開始していくのですが、このフェーズで、エンジニアとして大事な心の準備が アイデアを全て形にする意気込みを持つ事 だと個人的には思っています。

作られたデザインは、数ある候補の中から、一番成功の角度が高そうだと思うデザインの 1 案であって、完成ではありません。実際に実装してみて、使って見て、何度もブラッシュアップを繰り返したり、時には、全く別のアプローチを取る為に白紙に戻したりをしながら、最終的に完成形が見えてくるものです。実際に本番と同じぐらいのデータ量で、実機で確認してみないと判断がつかない事も多く、自分の端末でいつもと同じようにお出かけ先を探してみて、初めて見えてくるいい点や悪い点などの発見があり、その発見からまた新しいデザイン案が生まれたりします。

最終的な完成形に向かって試行錯誤を繰り返していくフェーズで大事なのは、もらったデザインの細部まで実装を模写する事ではなく、荒削りの動くベータ版を作って、実際に触ってもらっていい点と悪い点を探ることにあります。

ここで「もらったデザインが完成品だと思ってました」みたいな意識で開発を進めていると、「ここを変更してほしい」といった要望に対して快く受け入れる事が出来なかったり、「ここのデザインどう思う?」といった相談に対して、「今のままがいいと思いますよ(もう実装しちゃっているからな)」とか言っちゃったりして、もうプロジェクト終了です。

いこーよアプリの場合は、週末に子どもと遊びに行く場所を探す事が多いので、1 週間で実装できるところまで荒削りの状態でもいいので行い、実際に子どもと遊ぶ場所を探してみて、良し悪しを確認するといったことをほぼ毎週末行いました。そこで得られた知見を踏まえて、次の 1 週間でさらにブラッシュアップするといった事を繰り返し、とても疲れましたが、納得感を高めながら完成形まで持っていくことができました。

アイデアを全て形にする意気込みモードの時は、自分の場合は、コードの品質は全て捨て、動くモックを完成させる事に全ての注力を注ぎます。その時は、利用できるライブラリがあれば、迷わず組み込んで、まずは動くものを最短で作れる事を優先します。最終的にこれで行こう!といった方針が決まったら、ライブラリの選定をやり直したり、用件に合わない場合は、ライブラリを利用しないように作り直します。また、コードの品質を捨てていた部分のリファクタリング、作り直しを行うようにしています。

まさに、この記事で語られている「楽しい!喜んで!」の部分です。

logmi.jp

悪かった点

悪かった点は、今後の開発で、同じ失敗をしない為の備忘録です。今回のプロジェクトで一番悪かった事は 「必要な機能」と「あれば嬉しい機能」を、第 1 弾のデザインが上がってきた時から、振り分けをしていなかった 事です。

良かった点で挙げたように、リニューアル規模のプロジェクトを進める場合は、最初のデザインは完成されたデザインではありません。ブラッシュアップを重ねる事で完成度が高まっていくので、何度もデザインの作り直しと実装の変更が発生する為、完成までのスケジュールの見通しが非常に悪くなります。そのような中で、スケジュールの遊びとなる部分は「あれば嬉しい機能」の開発期間です。必要な機能から開発を進め、納得の行くところまで作り、完成後に「あれば嬉しい機能」の実装に入れば、あれば嬉しい機能がスケジュールに間に合わなければ、後日対応に回すだけですむ為、ブラッシュアップに集中する事ができます。

今回は、必要な機能とあれば嬉しい機能の振り分けをきちんとしていなかった為、両方の機能の実装を並行して進めていました。ブラッシュアップに時間がかかった為、リリースまでの期間が短くなり、あれば嬉しい機能の中から、いくつかの機能はリリースを見送る事になりました。見送った機能の中には、実装が 8 割ぐらい完成しているものがあったりと、あれば嬉しい機能で使った時間を必要な機能の方に最初から回せていれば、もっと効率的に良いものが作れたはずです。

まとめ

良かった点は アイデアを全て形にする意気込みで開発に臨めた 事で、これは今後も続けていきたいと思います。 悪かった事は 「必要な機能」と「あれば嬉しい機能」を振り分けなかった 事で、これは今後は、適切なタイミングで振り分けを行うように気をつけたいと思います。

こうやって、主体性を持ちつつ、いいものを納得いくまで作り上げていける環境は、3 年間積み重ねてきたチームメンバー同士の信頼関係があるからこそ、出来上がった環境だと思います。良いチームに巡り会えて本当に感謝しています。来年も頑張って、いこーよアプリを更にいいものにしていきたいと思います。

いこーよアプリチームでは、アプリの成長を加速させる為に、モバイルエンジニアを募集しています!

actindi.net

お気軽にご連絡お待ちしています!

actindiでの3年間のモバイルアプリ開発を振り返る

これはactindi Advent Calendar 2018、1日目の記事です。
Androidアプリエンジニアのhondaです。
実は今日でactindi勤続3年になります。
3年間、いこーよモバイルアプリを担当してきました。
今回は軽く3年間どのようにモバイルアプリ開発をしてきたのか振り返ってみたいと思います。

actindiにジョインした理由

そもそもactindiにジョインした理由ですが、前職で一緒に仕事したことがあり、先にactindiにジョインしていたディレクター兼デザイナーのsuzukiから「一緒にいこーよのモバイルアプリを作らない?」というお誘いを頂いたことがきっかけでした。

いこーよは世の中のパパ、ママに向けて子供とのお出かけ先情報を提供する情報サイトです。  当時のいこーよはモバイルアプリはなく、いこーよからファンのもとへお出かけ先情報を提供していくためにモバイルアプリを作ろうというフェーズでした。
カジュアル面談や面接を受けて、以下の点でジョインを決めました。

  • actindiは当時から個人の自主性を重んじたり、ルールでがんじがらめにするのではなく、自分たちで「一番アウトプットを出す方法は何か?」を考え、実践することを大事にしていました。そこに強く共感することが出来ました。会社のホームページにも掲載されています。
    actindi.net 実際に入社してからもちゃんと文化として定着していて、リモート勤務や何時から何時まで働くか?ということが自分で判断して決めることができるので子持ちの身としてはとても助かりました。
  • いこーよは当時知らなかったのですが、1歳の娘がいたので、サービスにとても魅力を感じた。
  • 最初はiOSアプリを作るがAndroidアプリも作る予定なので今までのAndroid開発経験を活かせる。
  • お給料が上がった。

開発スタート

モバイルアプリ開発は私を誘ってくれたディレクター兼デザイナーと私の2名体制でスタートしました。
前述した通り、いこーよユーザはAndroidよりiOSが多いため、iOS開発から取り掛かりました。
最初の1ヶ月はモバイルアプリの目的を明確にしてペルソナの設定、盛り込む機能/盛り込まない機能の決定、UIの検討&調査に費やしました。
最初のGitHubリポジトリへのコミットは2015年12月28日でした。

技術選定も一任されていました。
方針としては凝ったことをせず、シンプルに作ることを心がけました。
アーキテクチャはMVCライク、データベースはRealm、ネットワーククライアントはAlamofireというような実績のあるものや使いやすいものを採用しました。
また、開発言語はSwiftを採用しました。Objective-CでiOSアプリを作っていたことがあったのでSwiftの書きやすさに感動したものです。
CIはサーバサイドがJenkinsを使っているのでモバイルアプリでもJenkinsを使おうか?とも考えましたが、環境構築に時間をかけたくなかったので、bitriseを使うことにしました。 qiita.com

iOS版開発にWebエンジニアがジョイン

年明けからWebエンジニアでiOSアプリ開発経験もあったnamikataがアプリチームに参画し、チーム3名体制になりました。 チームメンバー全員でのデザインの共有や議論はProttを使って行いました。
実装は私とnamikataで作業を分担して、お互いにコードレビューしながら進めました。
二人ともSwiftはほぼ初心者の状態でしたが、コードレビューで議論しながら開発を進めて行けたことはとても有意義でした。

Swiftに興味のあるWebエンジニアの方からもコードレビューをしていただける機会があったのでとてもありがたかったです。このようなエンジニアがプラットフォームを超えて、プロダクトに対してバリューを出せるのはactindiの強みだと思います。

また、アプリチーム全員が子持ちのお父さんだったこともあり、「親としてどういうアプリだと嬉しいか?」というようなユーザ目線でのサービス開発が出来ました。
actindiに入社する前はチーム全体でユーザ目線で開発するという経験が少なかったので大変でしたがとても楽しかったです。

iOS版いこーよリリース

そして、2016年7月にiOS版いこーよがリリースされました。
2018年11月現在、iOS版のダウンロード数は11万強に到達しました!

f:id:kou_hon:20181128154657p:plain 上の画像はバージョン1.0.0のUIです。
最初のころのUIは自宅や現在位置、お出かけ先でのスポットを直感的に見つけられるようにマップをメイン画面にしました。
また、行きたいスポットを見つけたら保存しやすくするためボタンを大きめにしたり、行ったスポットの口コミを書きやすくるために入力エリアをわかりやすくしたり、書きやすくするような配慮をUI設計で行いました。

Android版開発スタート

iOSがリリースした後、2016年8月からAndroid版の開発がスタートしました。
基本的にはiOSで実装したUIや機能をAndroid版に移植するような流れでした。
が、Android版ではiOS版をそのまま移植してしまうとAndroidアプリらしさがないものになってしまいます。
Androidユーザの混乱を少しでも軽減するためMaterial Designに則ったUIにするようにしました。 アーキテクチャはiOS版の開発で出てきた課題を鑑みつつ、最初はMVC+レイヤードアーキテクチャーのようなものにしました。
データベースはiOSで使い慣れたRealm、ネットワーククライアントはRetrofitを採用しました。 Android版でもシンプルな設計を目指しました。

言語は私の提案でKotlinにしました。Kotlinはいいぞ。 tech.actindi.net

CIはAndroid版でもbitriseにしました。 現在、iOS版とともにbitriseでプルリクごとにビルドして、deploygateで社内配信する構成になっています。

息子誕生

私事ですが、Android版開発中に息子が誕生しました。
この頃から自宅でのリモート勤務が多くなってきましたが、前述した通りチームメンバーがみんな子持ちなのでメンバーがお互いを考えながらフォローしあえました。
actindiはコアタイムの無いフレックス制を導入しています。子供の風邪などの急な対応がしやすくとてもありがたかったです。
こういう時、「周りに迷惑をかけて申し訳ない・・・」と思いがちですが全社的に「お互い様」の考え方が定着しているので、安心してリモート勤務や勤務時間を変更したりすることができました。
アウトプットも極端に低くなったということはなく、チームに貢献することができたと思います。

Android版いこーよリリース

2017年2月にAndroid版いこーよがリリースされました。
2018年11月現在、Android版もダウンロード数が11万強に到達しました!

f:id:kou_hon:20181130135556p:plain:w320

Android版はiOS版よりシンプルなUIになっていますが、スポットの検索性や口コミのユーザ体験はiOSと同じになるように実装しました。

アプリの成長

Android版リリース後、iOS版、Android版ともにFirebaseAnalytics、GoogleAnalyticsを使ったユーザ行動解析、アプリ内でのユーザアンケート、などなどいろいろな角度からいこーよユーザにとっての使いやすいアプリを目指し改修を進めていきました。
毎週月曜日は定例ミーティングを開き、上記のデータやKPI、KGIをチーム全体で確認しあい、アプリとしてどういった成長が必要なのかを話し合っていきました。
そのミーティングでエンジニア、ディレクターの垣根を超えて、施策の提案なども行い、チームメンバー全員が施策に対してポジティブな意見で肉付けをしていき、具体的にサービスづくりに落とし込んでいきました。
小さいチームならではの動き方ではあると思いますがactindiらしい良いチームの動きだなと感じています。

UIの変化

下の画像は2018年11月現在のAndroid版、iOS版のメイン画面です。リリース当時に比べてだいぶ変わりました。
Android版はマップベースですが、iOS版はさらに普段遣いができるアプリを目指すため、週末に子供とのお出かけ先をすぐに見つけられるように季節、イベント、口コミ、クーポンなど様々な角度からおすすめのスポット情報を提供できるUIにしました。

f:id:kou_hon:20181130135235p:plain:w290 f:id:kou_hon:20181130134850p:plain:w290

アーキテクチャー

アプリのアーキテクチャーも最初のころとは変化していきました。
namikataがブログでiOS版の実装について触れていますのでぜひ御覧ください。 tech.actindi.net tech.actindi.net tech.actindi.net

Android版はMVCからAndroid Architecture Componentsをベースにしたものに設計方針を変え、よりテスタブルで追加実装しやすい設計に変更しています。詳しいことは別のエントリでお伝えできればと思います。

不具合対応

運用初期のころは毎朝、Google Play ConsoleやFirebaseをチェックしていち早く不具合に対応するように心がけていました。
今ではクラッシュ通知をslackに通知するようにして、より早く不具合に対応できる体制にしています。段階的リリースの利用やリリースタイミングなどを臨機応変に変えていきながら、安全にサービスを提供できるように心がけています。

評価

これまでお伝えしたアプリの成長によって、いこーよユーザの方々からAndroid版、iOS版ともに4.4の評価をいただきました。ありがとうございます!

f:id:kou_hon:20181130135858p:plain:w290 f:id:kou_hon:20181130135948p:plain:w290

これからのモバイル版いこーよアプリ

上記の現状のiOS版のUIでいい結果を残せたのでそのUIをベースにした新UIを目下開発中です。
iOS版のUIでの反省点や改善点を踏まえつつ、Androidらしい新UIを目指していきます。
iOS版、Android版ともにこれからもいこーよユーザにとって使いやすいアプリを目指すために成長させていきます。ご期待下さい!

まとめ

actindiでの3年間のモバイルアプリ開発の中でチームメンバー同士助け合い、より良いアプリを作るために議論し、いこーよモバイルアプリを成長させてきました。
そんないこーよアプリチームではモバイルエンジニアを募集しています!

actindi.net

いこーよアプリチームに興味がある方、いこーよアプリに興味がある方、いこーよアプリチームメンバーと話してみたい方などなど、ご連絡お待ちしています!

アーキテクチャを導入する前にできるコードのメンテナンス

いこーよの iOS アプリの開発を担当している namikata です。最近 iOSアプリ設計パターン入門 を読んで設計の勉強をしていますが、設計っていつ考えても難しいですね。猫型蓄音機さんがまとめてくれている実況シリーズを何度も読み直させてもらってます。

nekogata.hatenablog.com

MVVM や iOS Clean Architecture など、 MVC 以外の色々なアーキテクチャに興味はあるのですが、何分 1 人で iOS を開発をしていて、いこーよアプリもまだリリースしてから 2 年ぐらい。 Swift や 各種ライブラリのアップデートも頻繁に行える規模の為 、アーキテクチャを導入することで解決したい課題がそんなに大きな比重を占めてなく、一緒に働くメンバーが増えたら MVC 以外のアーキテクチャの導入を検討するのがいいのかなぁと悶々と考えています。なので、今回は、設計に関わらず、開発を続けて行く上で意識しているコードのメンテナンスに関わる事について、つらつらと書いて行きたいと思います。

共通の性質や振る舞いを Protocol にまとめて共通化する

以前記事にした ViewController での共通の振る舞いを Protocol で共通化する事も、コードのメンテナンス性の向上の一貫だと思っています。

tech.actindi.net

複数箇所で採用される為、 Protocol には、説明用のコメントと合わせて、動画 GIF で詳細を語るようにしています。

f:id:t-namikata:20181122111856p:plain

コメントにリンクされている画像はこちらです

f:id:t-namikata:20181122112014g:plain:w200

最初は、コメントを詳細に残すようにしていたのですが、中々イメージが伝わりにくいので、動画 GIF を撮影して、コメント欄にリンクを貼る事にしました。 動画 GIF の撮影は LICEcap で iPhone シュミレーターを使って作成し、動画 GIF のアップロードは github に適当な issue を立てて、そこに放り込んで参照する url を作成しています。利用されているシーンを実際に見てから、コードに目を通す事で、理解しやすいのではないかと思っています。

Presentation Domain Separation はしっかり意識

Model 層(View と Controller 以外)では、 UIKit の import を極力しないように意識します(画像転送時とかに UIImage そのまま渡したりしている箇所は多々ありますが...)。その意識があるだけで Model 層の単体テストがとてもやりやすくなります。

ライブラリに頼る

ライブラリに依存する事には一長一短あるかと思いますが(Swift 言語のバージョンが上がった時に、利用しているライブラリの対応を待つ必要がある。技術ノウハウが蓄積されていかない 等)、いこーよアプリでは、各種ライブラリを積極的に利用させてもらっています。 ライブラリを利用する目的には「導入する事で開発コストが抑えられて、サービス独自の機能開発に注力できる」事と「Utility や Extension の独自実装を減らして、コードのメンテナンス性が上がる」点が挙げられると思います。今回はコードのメンテナンス性をあげる為に利用しているライブラリを紹介したいと思います。

  • SwifterSwift
    • 便利な Extension をまとめてくれているライブラリ。今までは独自で Extension や Utility を沢山追加していましたが、現在は SwifterSwift で定義されていないものを適宜追加するようにしています。SwifterSwift に慣れておくと、他のプロジェクトでも、ライブラリを入れれば、慣れているメソッドで開発を進める事ができます。
  • SwiftDate
    • 日付に関する処理をまとめてくれているライブラリ。 Date から String への変換処理は頻繁に行われる為、ライブラリを利用する事で、書き方を統一したり、ミスのしやすいタイムゾーンの考慮をなるべく減らせるように努めています(日時の変換は、独自の実装で何度もミスをやらかしました...ついこの間も、日付を String に変換する処理をミスしてしまい 9 時間の誤差を...きちんと単体テストを書きました...)
  • SwiftyUserDefaults
    • UserDefaults を扱いやすくしてくれるライブラリ。こちらも UserDefaults を利用する際の書き方を統一してくれる目的で利用しています。

github.com

github.com

github.com

ライブラリを利用する際には、ライブラリの利用をいつでも辞められる覚悟を持てるものだけ採用するようにしています。Swift は頻繁に言語のアップデートが行われるので、利用しているライブラリが対応してくれないと開発が止まってしまうリスクがある為です。

ライブラリを利用する為のラッパークラスを用意できそうだったらする

画像を非同期で取得するライブラリを例にあげると SDWebImage や PINRemoteImage , Nuke など様々あり、多くの記事でベンチマークによる速度比較や、機能の比較を行っており、将来、利用するライブラリを変更する可能性があります。いこーよアプリではローディングを表示するライブラリに SVProgressHUD を利用していますが、こちらに関しても同じ事が言えます。

将来ライブラリを変更する可能性のあるものを利用する時には、ライブラリを包むラッパーを用意して、コードの各所で利用する時は、ラッパー経由で利用するようにしておくと、ライブラリの変更が用意に行えるようになります。

import PINRemoteImage

extension UIImageView {
    func setPhoto(urlString: String?, emptyImage: UIImage? = nil) {
        guard let url = urlString, url != "" else {
            image = emptyImage ?? UIImage(named: "dammy_image")
            return
        }
        let placeholderImage = emptyImage ?? UIImage(named: "loading_image")
        let errorImage = emptyImage ?? UIImage(named: "error_image")
        pin_setImage(from: URL(string: url), placeholderImage: placeholderImage) { [weak self] result in
            if let _ = result.error {
                self?.image = errorImage
            }
        }
    }
}

いこーよアプリでも SDWebImage から PINRemoteImage に変更したのですが、修正したメソッドは 2 箇所のみで、簡単に切り替える事ができました。

iOS の対応バージョンは 2 つまでを意識する

iOS がメジャーアップデートされるタイミングで、アプリの対応する iOS のバージョンの見直しを行います。基本的には、対応する OS のバージョンは 2 つまでで、最新と一つ前のバージョンです。現在では iOS12 と iOS11 になります。 iOS12 がリリースされて 2 ヶ月ほど経ち、iOS10 以前を利用しているユーザーは 5% 未満になった為、次のバージョンアップで、アプリの対応する OS を 11 以降とすることが決まりました。 OS の対応するバージョンを絞る事で、一部アプリをアップデートできなくなってしまうユーザーの方々が出てしまい、利用してくれている皆さんには申し訳ない気持ちになりますが、より良い機能を提供して、OS をバージョンアップしてもらおうという方向性です。iOS 11 以降を対象とする事で、 OS のバージョンによる条件分岐を書く必要がなくなり Vision.framework もスムーズに利用を開始する事ができました。

常に削除する機能を検討する

いこーよアプリチームでは、週に 1 度 MTG を行っているのですが、「この機能いらないのでは?」と思ったら、 MTG のアジェンダに記入し、相談として挙げて、削除するか残すかを毎週検討しています。完成して動作しているので、ユーザーにデメリットが無ければ残していいかもと思ってしまいそうですが(実装に時間がかかった部分なら尚更)いりません。泣きながら削除します。

これより以下は他の人には全くオススメできませんが、自分のスタイルに合っているので、おまけ程度に載せておきます。

ビルドエラー駆動開発

1人で開発しているので、コンフリクトの心配はほとんどなく、改修が入る度に、気になった所はリファクタリングを行っています。単体テストを書いてある事が理想だと思うのですが、現在のいこーよアプリのステージでは、

  • ミスが起きやすく、クリティカルな問題に繋がりやすい -> 積極的にテスト書こう
  • ミスは起きにくいが、起きた場合はクリティカルな問題に繋がりやすい -> モチベーションが上がっている時にテスト書こう
  • ミスは起きやすいが、クリティカルな問題に繋がりにくい -> ミスが起きた時はユーザーの皆さんと社員の皆さんに土下座
  • ミスが起きにくく、クリティカルな問題に繋がりにくい -> 目をつぶる

のように自分では区分けをしていて、テストでカバーできる範囲は結構少ないです。なので、 Xcode の IDE による error と Warning の検証にどっぷり浸かっています。

邪道な方法ですが、リファクタリングをする時は、クラスやメソッドなどがどこで使われているかは一旦気にせず、リファクタリングを進め、適当なタイミングでビルドを行って、Xcode でビルドエラーになっているところをチェックして改修するといった事をやっています。大々的にリファクタリングした時は、リリース前の目視による動作確認で担保します。まだ画面数も少なく、規模の小さいアプリなので出来る技だと思うのですが、これが一番早いです。このやり方には問題点が沢山ありますが、いいところもちょっとあります。それは、ビルドを何度も行うので、常にビルド時間は短く保とうといった意識が芽生える事です。こまめにビルドを行うので、差分ビルドの時間が一番重要になってきます。

未来の自分に期待する

今の自分の実力で綺麗に書くより、来年の成長した自分でリファクタリングした方がいいという発想です(単に現実から目を背けているといった見方も出来る)

最後に

アクトインディでは、エンジニアを募集しています!

actindi.net

五反田ランチはこのお店がオススメです!

カプリス (Caprice)

UIViewController の画面遷移などの振る舞いを protocol で実装する

いこーよの iOS アプリの開発を担当している namikata です。今回は UIViewController の画面遷移などの振る舞いを protocol で実装する方法を紹介したいと思います。色々な機能の実装を進めていく上で、こう書いたら使い回しきくしいいんじゃないかなぁ、と考えながら辿り着いた実装なので、まだ見えていない問題点があるかもしれませんが、自分の中では、開発効率が上がっていい感じで機能しています。

開発に携わっている いこーよアプリ をサンプルに書いて行きたいと思います。画面遷移などのアクションを protocol で書く事になった発端は、トップページ ( TopViewController ) のここの処理から始まりました。

f:id:t-namikata:20180921161029p:plain:w300

ユーザーさんが投稿してくれた口コミの一覧なんですが、CollectionView で実装していて、このセル( ReviewCell )では以下のアクションを行っています。

  1. 画像をタップすると遊び場施設の詳細画面に遷移する
  2. ユーザーアイコンをタップするとユーザー画面に遷移する
  3. 口コミのもっと見るをタップすると口コミ詳細画面 ( ReviewViewController ) に遷移する

まずは何も考えずに delegate で以下のような感じで実装を開始しました。

protocol ReviewCellDelegate : class {
    func transitionFacility(_ facility: Facility) // 遊び場施設の詳細画面に遷移する処理
    func transitionUser(_ user: User) // ユーザー画面に遷移する処理
    func transitionReview(_ review: Review) // 口コミ詳細画面に遷移する処理
}

class ReviewCell: UICollectionViewCell {
    private weak var delegate: ReviewCellDelegate?

    // 実際には facility, review, user などのデータを渡してあげます
    func setup(delegate: ReviewCellDelegate) {
        self.delegate = delegate
    }

    @IBAction func facilityClicked() {
        delegate?.transitionFacility(facility)
    }

    @IBAction func userClicked() {
        delegate?.transitionUser(user)
    }

    @IBAction func reviewClicked() {
        delegate?.transitionReview(review)
    }
}

class TopViewController: UIViewController, ReviewCellDelegate, UICollectionViewDataSource {
    func transitionFacility(_ facility: Facility) {
        // 遊び場施設の詳細画面に遷移
    }

    func transitionUser(_ user: User) {
        // ユーザー画面に遷移に遷移
    }

    func transitionReview(_ review: Review) {
        // 口コミ詳細画面に遷移
    }

    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "Cell", for: indexPath) as! ReviewCell
        cell.setup(delegate: self)
        return cell
    }
}

delegate パターンで実装しました。次に、口コミ本文のもっと見るをタップした時に遷移する、口コミ詳細画面 ( ReviewViewController ) の実装に移りました。口コミ詳細画面 はこんな風になっています。

f:id:t-namikata:20180921155814p:plain:w300

ここで口コミ詳細画面で行えるアクションを見てみると

  1. 施設名をタップするとスポット詳細画面に遷移する
  2. ユーザーアイコンをタップするとユーザー画面に遷移する
  3. 参考になった を押すと口コミに参考になったができるようになる。

といった内容になっていて、先ほどの ReviewCell と 1 〜 2 は同じアクションで 3 が追加された感じになっています。 ReviewViewController を愚直に実装すると以下のようになりました。

class ReviewViewController: UIViewController {
    @IBAction func facilityClicked() {
        // 遊び場施設の詳細画面に遷移
    }

    @IBAction func userClicked() {
        // ユーザー画面に遷移に遷移
    }

    @IBAction func likeClicked() {
        // 未実装
    }
}

TopViewController の transitionFacility と ReviewViewController の facilityClicked は同じように遊び場施設の詳細画面に遷移するアクションです。それぞれ画面遷移する処理を書いてあげればいいのですが、よくよく考えて見ると、

  • 口コミされた施設に遷移する
  • ユーザーをタップしたらユーザー画面に遷移する
  • 一覧の場合は、口コミ本文をタップしたら口コミ詳細が見れる
  • 口コミにいいねができる

といったアクションは、口コミに対するアクションとして、口コミを表示している他の画面でも使いそうに思えてきます。元々 ReviewCellDelegate で定義しているのは、口コミに対して行えるアクションになっているので、その振る舞いを protocol extension で実装してあげれば、色々使い回せるんじゃないかと思いました。

ReviewCellDelegate を ReviewActionable に名前を変更し、以下のように extension を書いてみます。

protocol ReviewActionable : class {
    func transitionFacility(facility: Facility) // 画像をタップした時の処理
    func transitionUser(user: User) // ユーザーをタップした時の処理
    func transitionReview(review: Review) // 口コミ本文のもっと見るをタップした時の処理
}

extension ReviewActionable where Self: UIViewController {
    func transitionFacility(_ facility: Facility) {
        // 遊び場施設の詳細画面に遷移する処理
    }

    func transitionUser(_ user: User) {
        // ユーザー画面に遷移に遷移する処理
    }

    func transitionReview(_ review: Review) {
        // 口コミ詳細画面に遷移する処理
    }
}

そして ReviewViewController で ReviewActionable を採用する事で、口コミに対する振る舞いを手に入れることができるようになりました。

extension ReviewViewController: ReviewActionable {}

class ReviewViewController: UIViewController {
    @IBAction func facilityClicked() {
        transitionFacility(facility)
    }

    @IBAction func userClicked() {
        transitionUser(user)
    }

    @IBAction func likeClicked() {
        // 未実装
    }
}

TopViewController からは ReviewCellDelegate ( 現 ReviewActionable ) に関する記述が消え、以下のようになりました。

extension TopViewController: ReviewActionable {}

class TopViewController: UIViewController, UICollectionViewDataSource {
    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "Cell", for: indexPath) as! ReviewCell
        cell.setup(delegate: self)
        return cell
    }
}

ただ TopViewController から、遷移する処理がバッサリ消えてしまったので、どこで何をやっているのか、コードの可読性が落ちるんじゃないかといった懸念があり、今、色々試験運用中です。 口コミ詳細画面では、口コミに対して「いいね」ができるので、その実装も protocol に追加して見ましょう。

protocol ReviewActionable : class {
    // ... 略 ...
    func likedReview(_ review: Review) // 口コミにいいねする
}

extension ReviewActionable where Self: UIViewController {
    // ... 略 ...
    func likedReview(_ review: Review) {
        // 口コミ登録アクション
    }
}
extension ReviewViewController: ReviewActionable {}

class ReviewViewController: UIViewController {
    // ... 略 ...
    @IBAction func likeClicked() {
        likedReview(review)
    }
}

こうする事で、今は必要ないですが、 TopViewController でも、口コミにいいねをしたいとなったら、既にいいねを行うアクションを protocol extension で定義してあるので、実装の追加も容易に行えます。開発中に、横スクロールだけでなく、縦スクロールで、もっと口コミが見れたらいいよね、って事で、機能が追加され、もっと見るボタンが設置されました。

f:id:t-namikata:20180921155925p:plain:w300

もっと見るを押した時の遷移先のページ ( ReviewListViewController ) はこのようになっています。

f:id:t-namikata:20180921155849p:plain:w300

TableView で実装することになっても、対応は簡単です。

extension ReviewListViewController: ReviewActionable {}

class ReviewListViewController: UIViewController, UITableViewDataSource {
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) as! ReviewTableViewCell
        cell.setup(delegate: self)
        return cell
    }
}

class ReviewTableViewCell: UITableViewCell {
    private weak var delegate: ReviewActionable?

    func setup(delegate: ReviewActionable) {
        self.delegate = delegate
    }

    @IBAction func facilityClicked() {
        delegate?.transitionFacility(facility: facility)
    }

    // ... 略 ...
}

このようにして、複数画面で UIViewController の振る舞いに関する処理を protocol を利用してスムーズに開発する事が出来ました。最終的なコードは以下のようになりました。

// UIViewController が口コミに対して行うアクションをまとめた protocol
protocol ReviewActionable : class {
    func transitionFacility(_ facility: Facility) // 画像をタップした時の処理
    func transitionUser(_ user: User) // ユーザーをタップした時の処理
    func transitionReview(_ review: Review) // 口コミ本文のもっと見るをタップした時の処理
    func likedReview(_ review: Review) // 口コミにいいねする
}

extension ReviewActionable where Self: UIViewController {
    func transitionFacility(_ facility: Facility) {
        // 遊び場施設の詳細画面に遷移
    }

    func transitionUser(_ user: User) {
        // ユーザー画面に遷移に遷移
    }

    func transitionReview(_ review: Review) {
        // 口コミ詳細画面に遷移
    }

    func likedReview(_ review: Review) {
        // 口コミ登録アクション
    }
}
// トップ画面
extension TopViewController: ReviewActionable {}

class TopViewController: UIViewController, UICollectionViewDataSource {
    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "Cell", for: indexPath) as! ReviewCell
        cell.setup(delegate: self)
        return cell
    }
}

class ReviewCell: UICollectionViewCell {
    private weak var delegate: ReviewActionable?

    func setup(delegate: ReviewActionable) {
        self.delegate = delegate
    }

    @IBAction func facilityClicked() {
        delegate?.transitionFacility(facility)
    }

    @IBAction func userClicked() {
        delegate?.transitionUser(user)
    }

    @IBAction func reviewClicked() {
        delegate?.transitionReview(review)
    }
}
// 口コミ詳細画面
class ReviewViewController: UIViewController {
    @IBAction func facilityClicked() {
        transitionFacility(facility)
    }

    @IBAction func userClicked() {
        transitionUser(user)
    }

    @IBAction func reviewClicked() {
        transitionReview(review)
    }

    @IBAction func likeClicked() {
        likedReview(review)
    }
}
// 口コミ一覧画面
extension ReviewListViewController: ReviewActionable {}

class ReviewListViewController: UIViewController, UITableViewDataSource {
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) as! ReviewTableViewCell
        // 本当は facility, review, user などの情報も渡す
        cell.setup(delegate: self)
        return cell
    }
}

class ReviewTableViewCell: UITableViewCell {
    private weak var delegate: ReviewActionable?

    func setup(delegate: ReviewActionable) {
        self.delegate = delegate
    }

    @IBAction func facilityClicked() {
        delegate?.transitionFacility(facility: facility)
    }

    @IBAction func userClicked() {
        delegate?.transitionUser(user)
    }

    @IBAction func reviewClicked() {
        delegate?.transitionReview(review)
    }
}

最後に

アクトインディでは、一緒に働いてくれるエンジニアを募集しております。 興味のある方はぜひお越しください!

VSCodeでFlutter、最初の一歩

Web エンジニアの morishitaです。

Web アプリは Android も iOS を1つのソースで動かせるのに、 それぞれ作らないといけないなんてネイティブアプリ開発は大変だなぁといつも横目でちら見しております1

プッシュ通知やオフラインキャッシュなどアプリ開発の強い動機になっていた機能が Webアプリでも実装可能になってきており、AndroidではPWAアプリをホームスクリーンに追加すると 単なるショートカットではなくネイティブアプリ同様に扱われるようになってきています2

それでも、端末の機能をほぼ制限なく使え、カメラやセンサやNFCなどを利用した3ネットとリアルのよりリアル寄りのアプリケーションを開発できるネイティブアプリの自由さはいいなぁと思うことがあります。

Webアプリのようにネイティブアプリを開発できる、AndroidもiOSもワンコードですと謳う開発環境やフレームワークはこれまでもありましたが、スマホの2大プラットフォーマーの1つGoogleを中心に開発が進められているものが出てきました。それがFlutterです。

真打ち登場かもしれません。ということで、ちょっと触ってみました。

Flutterとは

Google が公開しているスマホ向けネイティブアプリの開発プラットフォームと言うかフレームワークで OSS として開発されています。

flutter.io

次の特徴を謳っています。

  • Fast development
    • ホットリロードでコードの変更がすぐに反映され、試しながら実装できる
    • カスタマイズ可能なUI部品
    • 1つのコードベースでAndroid、iOS のクロスプラットフォーム開発できる
  • Expressive and Flexible UI
    • マテリアルデザインとクパティーノ (iOS-flavor)のUIパーツ
    • 各プラットフォームに配慮したリッチな動きやスムーズなスクロール
  • Native Performance
    • iOSとAndroidの両方で完全なネイティブパフォーマンスを提供

また、実装言語にはDartを採用ししています。 サードパーティのパッケージを Flutter Packagesで探して利用することもできます。

このエントリではVSCodeでFlutterによるアプリ開発するためのセットアップからサンプルアプリの動作確認までやってみます。

セットアップ

以降はMac OS 上でのセットアップの流れです。

Android Studio と Xcode のインストール

あれ?VSCode で Flutter じゃないの? と思われるかもしれないですが、 最終的には Android /iOS 向けにビルドする必要があり、それぞれビルドするためのツールチェインが必要になります。そのためにインストールします。

まずはAndroidから。Android Studio をインストールしましょう。 Android に詳しい人は SDK だけでもいいかもしれないですが、そういう人はすでに Android Studio もインストールしているでしょう。多分。 詳しくない人は Android Studio をインストールしてしまうのが手っ取り早いし簡単です。

Android Studio のインストール  |  Android Developers

つづいて、iOS。 Xcode は App Store からインストールしましょう。

Xcode

Xcode

  • Apple
  • 開発ツール
  • 無料

Flutter SDK のインストール

お待ちかね、Flutterのインストールです。
インストールと言っても次のページから ZIP ファイルをダウンロードして展開するだけです。

flutter.io

展開したら、flutter というディレクトリができます。それを任意の場所に配置します。 私は ~/Libraryの下に置いています。

置き場所を決めたら次の様に flutter/bin にパスを通します。

export PATH=$HOME/Library/flutter/bin:$PATH

Flutter Doctorの実行

これで、flutter コマンドが使えるようになっているはずです。

ターミナルを開いて環境チェックコマンド flutter doctorを実行しましょう。

私の環境では次の様な結果が出力されました。

$ flutter doctor

  ╔════════════════════════════════════════════════════════════════════════════╗
  ║                 Welcome to Flutter! - https://flutter.io                   ║
  ║                                                                            ║
  ║ The Flutter tool anonymously reports feature usage statistics and crash    ║
  ║ reports to Google in order to help Google contribute improvements to       ║
  ║ Flutter over time.                                                         ║
  ║                                                                            ║
  ║ Read about data we send with crash reports:                                ║
  ║ https://github.com/flutter/flutter/wiki/Flutter-CLI-crash-reporting        ║
  ║                                                                            ║
  ║ See Google's privacy policy:                                               ║
  ║ https://www.google.com/intl/en/policies/privacy/                           ║
  ║                                                                            ║
  ║ Use "flutter config --no-analytics" to disable analytics and crash         ║
  ║ reporting.                                                                 ║
  ╚════════════════════════════════════════════════════════════════════════════╝

Doctor summary (to see all details, run flutter doctor -v):
[✓] Flutter (Channel beta, v0.7.3, on Mac OS X 10.12.6 16G1510, locale ja-JP)
[!] Android toolchain - develop for Android devices (Android SDK 28.0.1)
    ! Some Android licenses not accepted.  To resolve this, run: flutter doctor --android-licenses
[!] iOS toolchain - develop for iOS devices (Xcode 9.2)
    ✗ libimobiledevice and ideviceinstaller are not installed. To install, run:
        brew install --HEAD libimobiledevice
        brew install ideviceinstaller
    ✗ ios-deploy not installed. To install:
        brew install ios-deploy
    ✗ CocoaPods not installed.
        CocoaPods is used to retrieve the iOS platform side's plugin code that responds to your plugin usage on the Dart side.
        Without resolving iOS dependencies with CocoaPods, plugins will not work on iOS.
        For more info, see https://flutter.io/platform-plugins
      To install:
        brew install cocoapods
        pod setup
[✓] Android Studio (version 3.1)
[!] IntelliJ IDEA Ultimate Edition (version 2018.2.1)
    ✗ Flutter plugin not installed; this adds Flutter specific functionality.
    ✗ Dart plugin not installed; this adds Dart specific functionality.
[!] VS Code (version 1.27.2)
[!] Connected devices
    ! No devices available

! Doctor found issues in 5 categories.

PC 内の必要なモジュールがチェックされます。 [!]がついた項目が問題のある項目です。

これらを解消していきます。

Android 環境と iOS 環境の準備

上記の問題箇所に対処して、Android 環境と iOS 環境を整えます。

Android 環境

flutter doctorの結果の次の部分ですが、これは何やらライセンスに同意していないと注意されています。

[!] Android toolchain - develop for Android devices (Android SDK 28.0.1)
    ! Some Android licenses not accepted.  To resolve this, run: flutter doctor --android-licenses

指示に従って、flutter doctor --android-licensesを実行して同意すれば解決です。

次の項目ですが、IntelliJ についてはプラグインが足りないと言っているので、Flutter pluginDart pluginをインストールします。

[✓] Android Studio (version 3.1)
[!] IntelliJ IDEA Ultimate Edition (version 2018.2.1)
    ✗ Flutter plugin not installed; this adds Flutter specific functionality.
    ✗ Dart plugin not installed; this adds Dart specific functionality.

上記では Android Studio には問題ないですが、予めこれらのプラグインをインストールしていたためです。
インストールしていなけれは IntelliJ と同様のメッセージが出るので、それに従います。

iOS 環境

iOS toolchain についても解決方法が示されています。 それぞれ解決するための brewのパッケージインストールコマンドが示されているので、順に実行します。 上記のflutter doctorの結果の場合、次のコマンドを実行します。

$ brew install --HEAD libimobiledevice
$ brew install ideviceinstaller
$ brew install ios-deploy
$ brew install cocoapods
$ pod setup

brew install --HEAD libimobiledeviceが python2 に依存するらしく、python2 が無い環境だと自動的にpython2がインストールされます。 しかし、python2 にはpipが含まれないので、途中でコケます。
その場合、Installation — pip 18.0 documentationを参考にインストールして再実行すれば解決します。

また、cocoapodsのインストールには結構時間がかかるのでご注意を。

Flutter Doctorが解決方法も示してくれるのでそれに従っていけば割とスルッとできると思います。

VSCode と Flutter プラグインのインストールと設定

さて、いよいよ VSCode です。 Flutter Doctorの出力で次のように指摘されています。 特に解決方法が示されていませんが、単に Flutter プラグインがインストールされていないだけです。

[!] VS Code (version 1.27.2)

なので、次のプラグインをインストールしましょう。

marketplace.visualstudio.com

marketplace.visualstudio.com

インストールしたら、コマンドパレットからFlutter: New Projectを実行してみてください。

初めてだと次のようなダイアログが表示されると思います。
もし、プロジェクト名を聞いてきたら、飛ばして次に進んでいいです。

f:id:HeRo:20180920083921p:plain

Flutter SDK の場所がわからないと言っているだけなので、Locate SDKボタンをクリックして設定します4

設定したらコマンドパレットからFlutter: Run Flutter Doctorを実行します。
次のように結果がターミナルに出力されれれば OK です。

$ flutter doctor
Doctor summary (to see all details, run flutter doctor -v):
[✓] Flutter (Channel beta, v0.8.2, on Mac OS X 10.12.6 16G1510, locale ja-JP)
[✓] Android toolchain - develop for Android devices (Android SDK 28.0.1)
[✓] iOS toolchain - develop for iOS devices (Xcode 9.2)
[✓] Android Studio (version 3.1)
[✓] IntelliJ IDEA Ultimate Edition (version 2018.2.1)
[✓] VS Code (version 1.27.2)
[!] Connected devices
    ! No devices available

Connected devicesは気にしなくていいです。
Flutterプラグインから接続できる端末がないと行っているだけです。エミュレータを起動すれば解決します。

Flutter Doctorの結果がきれいになったところでセットアップは終了です。

サンプルプロジェクトで動作確認

さて、セットアップが終わったところで、サンプルを実行してみましょう。

プロジェクトの初期化

再び Flutter: New Project を実行します。 今度はプロジェクト名を聞いてくるので、適当に入力して保存する場所を選択したら、ファイル群が生成されプロジェクトが初期化されます。

この時点で、簡単なアプリのコードが含まれています。

仮想デバイスの準備

VSCodeのステータスバーに No Devices と表示されているのでクリックするとコマンドパレットに選択肢が表示されます。多分、iOS Simulator は最初から選択できるのではと思います。選択するとエミュレータが起動します。

f:id:HeRo:20180920235957p:plain

Android のエミュレータはCreate Newを選択すると自動的に作成されて起動します。

なお、作成するにはエミュレータのシステムイメージが必要です。 SDK マネージャを開いて、Android8.1Google play Intel x86 Atom System Imageをインストールすると、作れるようになります。

f:id:HeRo:20180920084025p:plain

Create Newを実行すると次の仮想デバイスが作成されます。

f:id:HeRo:20180920084038p:plain

実行

エミュレータの準備ができたらサンプルアプリを動かしてみましょう。

VSCode のデバッグを開くと Flutter を選択できるようになっています。 選択して、実行するとプロジェクトがビルドされエミュレータで実行されます。

f:id:HeRo:20180920235031p:plain

サンプルアプリは右下のボタンをクリックすると数字がカウントアップするだけのシンプルなものです。

試しに、lib/main.dartを開いてFlutter Demo Home Pageの文字列を書き換えたり、UIの色を変更ししてみると、すぐにエミュレータで実行されているアプリに反映されます。
これが Flutter の特徴のホットリロードですね。素晴らしい。

f:id:HeRo:20180921000823g:plain

以上でサンプルアプリの動作確認も終了です。

おまけ

セットアップしてからこのエントリを書くまでの間に Flutter のバージョンが0.7.2から0.8.3上がってしまいました。 ちょうどいいのでアップデートも試してみました。

FLutter のバージョンアップは flutter upgradeコマンドで可能です。

flutter upgradeの実行の様子はちょっと長いので折りたたみます。 興味ある方は開いて見てください。

で、アップデート後、flutter --versionでバージョンを確認します。

$ flutter --version
Flutter 0.8.2 • channel beta • https://github.com/flutter/flutter.git
Framework • revision 5ab9e70727 (12 days ago) • 2018-09-07 12:33:05 -0700
Engine • revision 58a1894a1c
Tools • Dart 2.1.0-dev.3.1.flutter-760a9690c2

ちゃんと上がっていますね。 アップデート後、VSCodeからアプリを起動してみましたが問題ありません。 アップデートも簡単です。

まとめ

難しいハマりどころはなく、VSCodeでFlutter環境は作れます。アップデートも難しくありませんでした。

結局 Android Studioをインストールするので(しかも私は Intellijも使っていてAndroid Studioにも抵抗ないので) VSCodeを使うのは微妙な気もしないではないですが、選択肢があるのは良いことではないでしょうか。

Flutter自体がまだRelease Preview 2が出たところなので実戦投入には十分な検証が必要だと思うのですが、プロトタイピングしてみる程度なら十分使えそうです。 特にホットリロードは小気味よく、Web開発感覚でアプリ開発できそうです。

最後に

アクトインディではWebでもアプリでも、インフラでもいろいろやりたいエンジニアを募集しています。


  1. 両プラットフォームへの配慮は必要ですし、時には作り分けたりする必要もありますが。

  2. プッシュ通知欲しさに開発されたほぼWeb版と変わらない(しかも全面的にUIはwebviewだったりするような)ネイティブアプリはAndroidに於いては減ってくるだろうなぁと個人的に思っています。

  3. ブラウザでもだいぶサポートされてきていますが、安定性やパフォーマンス含め広く一般的に使える状況まではもう少し掛かりそうですね。

  4. ~/Libraryに SDK を置いている場合、ファイル選択ダイアログでこのディレクトリが表示されなくて選択できないかもしれません。ターミナルを開いてchflags nohidden ~/Libraryを実行すると表示されるようになります。

よく使う Auto Layout のテクニック集

iOS のアプリ開発を担当している namikata です。アクトインディに入社して、アプリ開発を担当するようになって 2 年強が経ちました。アプリの新規リリースから運用まで携わらせてもらう中で AutoLayout と向き合う事も多く、よく使う AutoLayout のテクニックをまとめていきたいと思います。

目次

  1. View を使った複数要素のセンタリング
  2. CustomView の高さを修正の度に変更したくないので Bottom >= 0 の制約で回避
  3. CustomView の Self-Sizing
  4. View の高さを複数の制約の優先度をコントロールして変更する
  5. tableView を Grouped にして、上下に余白を設ける

View を使った複数要素のセンタリング

画像とテキストなど横並びにしてセンタリングしたいケースなどでよく使います。StackView に入れて StackView をセンタリングするだけです。

f:id:t-namikata:20180816133714g:plain

CustomView の高さを修正の度に変更したくないので Bottom >= 0 の制約で回避

色んな画面で使うパーツは CustomView にして使い回すことが多いと思いますが CustomView の高さを AutoLayout で決定させる為に Bottom の制約が必要になる際に使えるテクニックです。

f:id:t-namikata:20180816133755g:plain

ScrollView + StackView の作り方

最近やっと慣れてきましたw

f:id:t-namikata:20180816134012g:plain

  1. ScrollView は縦だけでなく横にも Scroll できるので、縦のスクロールのみできるように 親View と scrollView で EqualWidth
  2. scrollView の中の要素は横にスクロールできる ScrollView の性質の影響を受けるので CenterX

CustomView の Self-Sizing

他の画面で使い回したい要素だから CustomView に切り出そうと思った時に、いつも頭をよぎるのが Auto Layout の問題でした。CustomView を作成した場合に StoryBoard で CustomView を放り込むと Y ポジションの制約を求められます。CustomView にする前は View の中に配置した要素の制約により、親 View の高さが決まっていた為、警告は出ませんでしたが、CustomView にすると View の中の要素は StoryBoard は認識しないので、高さの制約が必要になります。

f:id:t-namikata:20180816133858p:plain

高さの制約を与えると CustomView の高さを変更した時に、使っている箇所全ての高さを変更しなくちゃ行けない問題が出てしまうので、そんな時に使えるテクニックです。StackView を活用することになります。

f:id:t-namikata:20180816134104g:plain

  1. 動的に高さを管理したい CustomView を StackView の中に入れる
  2. StackView の Distribution を Equal Spacing にする

ビルドするといい感じになります。

f:id:t-namikata:20180816134140p:plain

View の高さを複数の制約の優先度をコントロールして変更する

ある条件によって View の高さを変えたい時に height 制約をコードで変更する事がありますが、以下のようにやってしまうと Storyboard で指定した高さの制約と二重管理になってしまいます。

@IBOutlet weak var viewHeightConstraint: NSLayoutConstraint!
if isEmpty {
    viewHeightConstraint.constant = 10
} else {
    viewHeightConstraint.constant = 100
}

最近は NSLayoutConstraint の isActivate を切り替える事で高さの切り替えを行う事が多いです。

  1. Storyboard で優先度 1000 と 750 の制約をそれぞれ設定し、優先度 1000 の方を IBOutlet で結ぶ

f:id:t-namikata:20180816134203p:plain

  1. コードで高さを切り替える
@IBOutlet var viewHeightConstraint: NSLayoutConstraint! // weak はつけない
viewHeightConstraint.isActive = false // 優先度 1000 の制約を無効にする事で 優先度 750 の制約が適用される

注意点としては weak をつけると isActivate を切り替えた際に、インスタンスが解放されてしまうので weak 設定はしません。画面切り替わり時に deinit が呼ばれ ViewController がきちんと破棄されていたので多分大丈夫。詳細はこちらにものっています https://stackoverflow.com/questions/38051879/why-weak-iboutlet-nslayoutconstraint-turns-to-nil-when-i-make-it-inactive

f:id:t-namikata:20180816134221g:plain

tableView を Grouped にして、上下に余白を設ける

時々スクロール領域内に余白を設けたい事があり、このテクニックを使います。

f:id:t-namikata:20180816134242g:plain

  1. TableView の Style を Grouped にする
  2. TableView の背景色を変更する
  3. Header の高さを変更したい場合はコードで指定する

なぜか Header の高さが storyboard で指定しても効きません。なんでだろう。高さを変えたい時はコードで指定してあげる必要があります。

func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
    return 50
}

f:id:t-namikata:20180816134321g:plain

最後に

今回利用したコードは github に上がっていますので、もしよかったらどうぞ。

アクトインディでは、一緒に働いてくれるエンジニアを募集しております。 興味のある方はぜひお越しください!

XCode 5 の CI ツール Bot を試してみました (1)

こんにちは ataka です。

XCode 5 から「Bot」と呼ばれる CI ツールが入りました。OS X Maveriks (10.9) と OS X Server 3.0 が必要です。といっても、Maveriks は無料でアップデートできますし、OS X Server は (通常 2,000 円 ですが) iOS Developer なら iOS Dev Center から無料でダウンロードできますので、iOS 開発者なら出費なく Bot の準備ができます。

とはいえ、設定が一か所に集中していない。細かなハマり所が多い。ということで、Bot を動かすのには随分と苦労させられました。特に既存のプロジェクトで Bot を作ろうとすると大変です。そこで、今回は新規プロジェクト「Foo」をローカルに作って Bot を動かしてみます。

Bot とは

bot1

スクリーン・ショットです。Bot を実行すると、コンパイルを実行し、テストを行ない、分析をして、ダウンロード用のプロダクト (ipa ファイル) やアーカイブを作ってくれます。

Bot の実行結果は XCode でも閲覧できますが、ウェブ・ブラウザーからも見ることができます。スクリーン・ショットは Bot の実行結果を Safari で閲覧したものになります。

まずは、このスクリーン・ショットを到達点にします。

Server の設定

OS X Server をダウンロードし、インストールします。起動してみましょう。初めて使う場合は Mac を選択する画面が現れます。ここでは自分の Mac を選択します。

起動すると、左カラムに「XCode」という項目があるので、選択します。

XCode の選択画面が現れます。複数の XCode を使い分けている人向けの設定と思いますが、普通に自分の使っている XCode を選びます。

server1

この画面では、以下の項目が設定できます:

ここで必要なのは XCode と開発チームの設定です。アクセス権やデバイスの設定は、後回しにしても良いでしょう。リポジトリの設定は (今回は) XCode 側から行なうので必要ありません。XCode の設定はほとんど一択なので迷いませんね。開発チームには、開発に使っている Apple ID を入力します。

設定を終えたら、ブラウザーで確認してみましょう。「アクセス」の「状況」に書いてあるホスト名でアクセスできます。私の場合、「mba-ataka.local」ですから、Safarimba-ataka.local/xcode を開きます。

空の Bot 画面が表示されるでしょう。

XCode の設定

XCode を起動して XCode > Preferences > Accounts を開きます。左下の「+」ボタンを押して、「Add Server...」を選択します。Bot で使う X Server を選びます。ご丁寧にサーバーが選択画面に現れています。これを選んで先に進むと、次の様なエラーが出ました。

The Xcode Service is not enabled on the server 'mba-ataka.local'. Contact the administrator for assistance.

仕方がないので、「Or enter a server address:」に「localhost」と入力して「Next」ボタンを押します。

xcode1

お次はユーザー名とパスワードの入力です。

ユーザー名にフルネーム「安宅 (会社)」を入力すると、ユーザー名とパスワードが違うと怒られました。日本語が通らないのかしらん? では /Users の下にある名前ならどうかしらん。「ataka_work」を入力します。パスワードは「安宅 (会社)」のものを使います。

xcode2

フゥ、なんとか通りました。

Foo プロジェクトの作成

XCode で新しいプロジェクト「Foo」を作ります。Product Name は「Foo」にしました。~/project/Foo にプロジェクトを置きます。

xcode3

プロジェクトの置き場を決める時に、「Source Control」にチェックを入れます。また、Create git repository on 「My Mac」を「localhost」に変更します (もし時間があったら、X Server > XCode > リポジトリ を見てみましょう。「ホストされたリポジトリ」に「Foo」「http://mba-ataka.local/git/Foo.git」が追加されているのが見れるはずです)。

Bot を作る前にもう一つ、Scheme の設定変更が残っています。

XCode のメニューから Product > Scheme > Manage Schemes... を選びます。

xcode4

右端の「Shared」にチェックを入れます。

xcode5

設定を保存したら、git commit して push します。Bot は git のローカル・リポジトリーではなく、push 先のリモート・リポジトリーをベースに動いてくれるようです。

Bot の作成

ようやく Bot が作る準備が整いました。

XCode から Product > Create Bot... を選択します。

bot2

全てデフォールト値で構いません。「Next」です。

bot3

スケジュール設定です。時間ごと、日ごと、週ごと。もしくはコミット時。もしくは手動の三種類が選べます。今回はテストなので手動に当たる「Manual」を選択しておきます。慣れたら、Bot の設定を変えると良いと思います。

また、この画面では、Bot を実行する時に「解析」「テスト」「アーカイブ」も行なうかどうか設定できます。デフォールトでは全てにチェックが入っています。そのままにしておいて良いでしょう。「Cleaning」は Bot 実行前に clean を実行するかどうかです。デフォールトではオフです。

bot4

バイスの設定画面です。デフォールトでは全ての iOSバイスになっています。私は「Specific Devices」から「iPhone Retina (4-inch)(7.0)」と「iPhone Retina (3.5-inch)(7.0)」を選びました。

bot5

最後はメール通知の設定です。「Create Bot」を押して Bot を完成させます。

自動で Bot が走って、結果が表示されます。ウェブ・ブラウザーでは http://localhost/xcode から確認できます。上のスクリーン・ショットと同じになったら成功です。

もう一度 Bot を実行

上のスクリーン・ショットでは一つテストがエラーになっています。おざなりですが、直してみます。

Foo Project から FooTests > FooTests.m を開きます。この中の testExample が原因です。

- (void)testExample
{
    XCTFail(@"No implementation for \"%s\"", __PRETTY_FUNCTION__);
}

必ず成功するように変えてみます。

- (void)testExample
{
    XCTAssertTrue(true, @"Test Example for \"%s\"", __PRETTY_FUNCTION__);
}

変更したら push を忘れずに。

Safari から右上の「インテグレート」を押します。手動実行なので...ね。

bot6

結果は成功です。気持ちがいいですね。

Bot の実行をコミットごとにしたり、毎日にしたり、失敗したらエラー・メールを飛ばしたり、設定を変えてゆくともっと便利で楽になると思います。