Quantcast
Viewing all 733 articles
Browse latest View live

多腕バンディットによる表示コンテンツの最適化

こんにちは。技術部検索グループの原島です。

Image may be NSFW.
Clik here to view.
f:id:jharashima:20141029104449p:plain

上の画像は、スマートフォン(ブラウザ版)で見たクックパッドの検索結果ページです。レシピだけでなく、ニュースも表示されていますね。献立や掲示板のスレッドなどが表示されることもあります。

クックパッドでは、検索結果ページに表示するコンテンツをクエリなどに応じて最適化しています。最適化は、膨大なログデータと最新の機械学習を用いることで、実現しています。このエントリでは、クックパッドにおけるコンテンツ最適化の裏側を紹介します。

最適化の背景

スマートフォンの普及に伴って、ユーザが利用するプラットフォームは PC からモバイルにシフトしつつあります。クックパッドにおけるモバイル利用者の割合も、ここ 2 年で 10% 以上増加しました。最近では、60% 以上のユーザがモバイルからアクセスしています。

ユーザの利用形態が変化すれば、検索結果ページもその変化に対応しなければなりません。PC とモバイルの違いは沢山ありますが、最大の違いは画面のサイズではないでしょうか。モバイルは画面が小さく、大きなものでも、せいぜい 6 インチ程度しかありません。

画面が小さいと、UI をシンプルに保つのが難しくなります。あれもこれもとコンテンツを表示すると、UI が煩雑になってしまうのです。そのため、あらゆるコンテンツを検索結果ページに表示するわけにはいきません。

そこで、クックパッドでは、表示コンテンツを最適化するという解決策に行き着きました。クエリなどに応じて最適なコンテンツだけを表示すれば、UI を煩雑にすることなく、必要な情報をユーザに提供できるのではないでしょうか。

このような背景で、コンテンツ最適化の取り組みは始まりました。

多腕バンディット問題

では、どうすれば最適化を実現できるでしょうか。クックパッドでは、機械学習の一つである多腕バンディット問題に注目しました。多腕バンディット問題は、「探索」と「活用」という二種類の行動を使い分けて報酬を最大化する問題です。

よく題材として挙げられるのが、スロットマシンです。複数のスロットマシンがあった時、得られる報酬を最大化したいとします。ただし、使えるお金には制限があります。以下、「腕」という言葉を多用しますが、これはスロットマシンのレバーを指します。

Image may be NSFW.
Clik here to view.
f:id:jharashima:20141029105437p:plain

まず、当たりやすいマシンを見極めたいというのが自然な発想です。そして、各マシンに少しずつ投資すれば、これを見極められそうです。このように、情報を収集するために腕を選択するのが探索です。

当たりやすいマシンを見極められれば、こちらのものです。そのマシンに集中的に投資することで、報酬を最大化できそうです。このように、収集した情報に基づいて腕を選択するのが活用です。

ポイントは、探索と活用がトレードオフということです。探索への投資を増やせば、当たりやすいマシンが分かってきますが、活用の機会が減ってしまいます。一方、探索への投資を減らせば、当たりやすいマシンが分からないので、活用が一か八かになってしまいます。

報酬を最大化するには、探索と活用のバランスを取る必要があります。バンディット問題は古くから研究されており、沢山のアルゴリズムが提案されてきました。epsilon-greedy や softmax、UCB などのアルゴリズムを用いることで両者のバランスを取ることができます。

また、ここ数年、コンテキスト付きバンディット問題が注目を集めています。これは、コンテキストごとに最適な腕が異なる題材で報酬を最大化する問題です。クエリやユーザに応じて最適な腕を推定できるため、広告の配信などへの応用が期待されています。

最適化の仕組み

クックパッドでは、バンディット問題(コンテキスト付き)を用いて、コンテキストごとに最適なコンテンツを表示しています。具体的には、コンテンツを腕、CTR を報酬、クエリなどをコンテキストとみなして、報酬が最大となるように腕を選択しています。

下図に最適化の全体像を示します。最適化を行うための処理はオンラインの処理とオフラインの処理に大別されます。まず、オンラインで探索と活用を行います。具体的な処理は以下の通りです。

  1. 一部の検索 PV を探索に、その他の PV を活用に割り当てます。
  2. 探索に割り当てた PV では a を、活用に割り当てた PV では b を行います。
    1. ニュースや献立などのコンテンツからランダムに一つを選択して、検索結果に表示します。また、コンテンツがクリックされたかどうかという情報をロギングします。
    2. 前日までに収集した情報に基づいてコンテンツを選択し、検索結果に表示します。

Image may be NSFW.
Clik here to view.
f:id:jharashima:20141027185122p:plain

オフラインでは学習と推定を行います。具体的な処理は以下の通りです。

  1. 前日のログを整理して、訓練データとテストデータを構築します。
  2. 直近 N 日の訓練データに基づいてバンディットの学習モデルを構築します(上図では N = 3)。どのコンテキストに対してどのコンテンツを表示するかが学習されます。
  3. 学習されたモデルを直近 M 日のテストデータに適用します(上図では M = 1)。翌日に訪れそうなコンテキストに対して表示すべきコンテンツが推定されます。

オフラインの処理が終われば、コンテキストごとに CTR がもっとも高くなると推定されたコンテンツがデータベースに保持されます。翌日の活用時には、これを参照して、コンテンツを表示しています。同時に探索も行われ、その情報は次の日の学習に利用されます。

結果

以下に、最適化の例を示します。それぞれ「ポトフ」と「おでん」の検索結果ページ(10 月 28 日の新着順)です。献立と掲示板のスレッドが表示されていますね。これらは、それまでのログから CTR がもっとも高くなると推定されたコンテンツです。

Image may be NSFW.
Clik here to view.
f:id:jharashima:20141029104917p:plain
    Image may be NSFW.
Clik here to view.
f:id:jharashima:20141029105111p:plain

では、実際、CTR はどうなったのでしょうか。探索時の CTR との対比で、活用時の CTR は 160% 以上になりました。このことから、上で紹介した仕組みによって、よりユーザが求めるコンテンツを表示できていることが分かります。

まとめ

モバイルは画面が小さいため、なんでもかんでも表示するわけにはいきません。UI をシンプルに保つため、必要最小限のコンテンツだけを表示することが望まれます。

クックパッドでは、これを実現するため、多腕バンディット問題を導入しました。日々蓄積されるログデータを活用して、コンテキストに応じて最適なコンテンツを推定しています。

結果、CTR が高くなるコンテンツだけを検索結果に表示できるようになりました。今後は、さらなる最適化を目指して、コンテンツやコンテキストを拡大していく予定です。


レシピ検索を改善する工夫

こんにちは。技術部検索グループの兼山(@PENGUINANA_)です。

クックパッドの中でレシピ検索はレシピをのせる人とさがす人をつなぐ大事な仕組みです。 今回はレシピ検索を運用改善していく上での工夫のうち、他の検索システムでも役立ちそうな内容を紹介させていただきます。

改善ポイントを発見しやすくする

工夫1. 検索語をモニタリングする

search monitorという社内ツールを作りました。前日の検索傾向を表示できます。

このツールは以下の作業をサポートします。

  • 検索語をUU(ニーズ)が多かった順に知る
  • キーワードごとに何人に使われたのかを知る
  • ユーザーが実際に目にする検索結果を素早く確認する
  • レシピが1品も見つけられなかったキーワードを知る

Image may be NSFW.
Clik here to view.
f:id:code46:20141030135326j:plain

他にも「キーワードごとにどのようなキーワードと組み合わされやすいか」、「その検索語のCTRはどの程度か」、「その検索語は何時頃あるいは何曜日によく検索されるのか」を探検できます。

こうしてユーザーのニーズを俯瞰でインプットすることで、 CTRが相対的に低いキーワードやニーズが高まっているキーワードを発見できます。

工夫2. 速度をモニタリングする

どんなシステムでも遅ければ使われないはず。速度は重要だと思います。 検索グループでは最低でも週次のミーティングでレスポンスタイムのグラフを確認します。変化があった時はチェンジログを確認して問題の修正を試みます。

Image may be NSFW.
Clik here to view.
f:id:code46:20141030135359j:plain

インフラ部のメンバーがレシピ検索の裏側で利用されているSolrのアーキテクチャについて紹介しています。高速化につながる部分もありますので興味のある方はご覧ください。

工夫3. ユーザーやスタッフに教えてもらう

レシピ検索を利用いただいている皆様から寄せられるご意見を全て検索グループのメールアドレスにも転送してもらうようにしてあり、レシピ検索に関わるご意見を全て確認しています。

(ご意見)

Image may be NSFW.
Clik here to view.
f:id:code46:20141030135431p:plain

ここから直接改善点を見つけることもあれば、ご意見ではなく(返信が必要な)問い合わせにつながったケースをユーザーサポート部から転送してもらい検索グループで改善しています。

(問い合わせ)

Image may be NSFW.
Clik here to view.
f:id:code46:20141030135457p:plain

これに加えてスタッフからの報告も頼りにしています。

数百人いるスタッフの全てを動員しても全てのキーワードの検索結果を確認することは出来ません。 しかし、スタッフは常に新しい流行や、多くの人の目に触れるキーワードに注目しているため、そこで見つかる改善点は影響の大きいものであることも多いです。

スタッフの目を活用すべく、改善点に気づいたらその場で教えてもらえるようにしました。

Image may be NSFW.
Clik here to view.
f:id:code46:20141030135510j:plain

クックパッドでは、スタッフユーザだけにGoogleフォームへのリンクが表示されるようにしています。このフォームからGoogleスプレッドシートにキーワードと違和感を感じた点を思いついたままに書いてもらい、検索グループが毎日確認し、修正できたら報告者にお知らせしています。 これまでに150以上の違和感や不具合が報告され、その多くは修正が完了しています。

工夫4. ログを分析する

レシピ検索に仮に違和感を感じたとしてもそれを教えてくれるとは限りません。普通はただ去るだけですよね。

明示的なフィードバックだけでなく、暗黙のフィードバックも読み取るべくログを活用しています。

例えば、CTRや検索結果リストのクリックパターンが期待通りかどうかなどを確認しています。NDCGや、クリックUUのうち上位に表示されたレシピをクリックしたUUの割合(検索結果の上の方が無視されているかどうか)などに注目しています。本来はランキングの良さを評価する尺度ですが、問題発見に目的をしぼって活用しています。

改善のアクションをとりやすくする

見つけた改善点をUU(インパクト)の大きいものから順に優先順位付けし、場当たり的に直していきます。

即座に解決することが困難なものはペンディングして覚えておき、類似の問題がまた発生したとき、時間をとって全てを同時に解決する方法を探ります。

即座に解決できる問題は速く解決し、より困難な問題に時間を割けるように改善スピードをあげる工夫をしています。

工夫5. 辞書管理ツールを作る

発見した問題のうち、分かち書き・同義語の間違いなどは辞書の修正で解決できます。いちいちデータベースを修正していると、これがとても億劫な作業であることに気が付きます。そこでこれを簡単にできるツールを実装しました。ツールから同義語や辞書の修正をできるようにすることで、問題をためこまずアクションできるようにしました。

また、修正履歴は全て保存されていて、それぞれの単語に関する変更を確認できます。 「どうしてこんなデータが入っているのだろう?」という時に履歴を確認できると意図が分かり便利です。過去と反対の変更をしてしまう心配もありません。

Image may be NSFW.
Clik here to view.
f:id:code46:20141030135530j:plain

工夫6. ログを活用する

データからユーザーのタスクを助ける情報を抽出して反映します。

例えば、レシピが見つかったクエリチェーン(同一セッション内での検索語の変遷)は、ユーザーの上手な検索の軌跡です。

このデータからあるキーワードの次に検索されるキーワードを抽出して「関連キーワード」として提案します。 こうすることで、一歩先を読んだ検索語の提案ができます。この仕組みに興味のある方はLinkedInのこちらの記事がオススメです!

こうした仕組みを随所に取り入れています。

工夫7. インデックスを高速化する

クックパッドでは全てのインデックスは毎日リビルドされます。

こうしておくことで、検索インデックスに対するほとんどの変更を遅くとも翌日に確認できるようになります。

まとめ

レシピ検索を改善していく中で、他の検索システムでも共通しそうな部分を2つのフェーズに分けてご紹介しました。

  • 問題を発見しやすくすること
  • 改善のアクションをとりやすくすること

今後は、より多くのフィードバックをユーザーやスタッフの負担にならない形で教えてもらいながら、反映する仕組みを洗練させたいと思っています。

また、UIやナビゲーションが重要というのは他の機能と変わりません。UIの改善にも取り組んでいきたいです。

検索UIに関連する記事:

KPTで粘り強く品質改善に取り組んだ話

Image may be NSFW.
Clik here to view.
f:id:y_310:20141030163053j:plain

はじめに

こんにちは、モバイルファースト室の@y_310です。 部署名からもお分かりの通りクックパッドでは今年からスマートフォンアプリの開発に特に力を入れて取り組んできました。

実際に昨年と比べて開発体制が大きく変化しています。以前はアプリ開発専門のエンジニアのみで開発していたものを、サーバサイドエンジニアもアプリ開発を学び、自分が所属する部署に必要な機能をアプリに実装するようになりました。

そのため、以前は2、3人のチームでの開発だったものが、現在は多い時には複数の部署にまたがって10人ほどのエンジニアが1つのアプリにコミットする状況になりました。

そのような環境の変化によりアプリの品質維持が大きな課題となり、この半年間継続的に品質改善に取り組んできました。今回はその改善プロセスについてご紹介したいと思います。

課題

取り組みを始める前は、様々な部分で課題がありました。 具体例を上げると、

  • リリース毎にリリース日を調整していたため、多部署が関わるようになった結果調整コストが増大した
  • テスト期間が明示的に取られていなかったため、開発者の意識による品質のばらつきが大きく単純な問題が見逃されがちだった
  • リリース作業の担当者が決まっていなかったためリリース直前の作業漏れが頻発していた
  • リリース直前の駆け込みコミットによりテストされていない変更がリリースされていた

他にも大小様々な問題が山積している状態で、それが結果としてアプリの品質を落とすことにもつながってしまっていました。

この状況を打開するために最初に実践したことがKPTによる振り返りです。

KPT

KPTとはKeep、Problem、Tryの略で振り返りのためのフォーマットの一つです。リリース後の振り返りで、続けたいこと、問題と感じたこと、次の開発期間で改善に取り組むことを上げることで課題の整理と継続的な改善を実践するために採用しました。 採用当初は単に問題を整理するツール程度の感覚がありましたが、半年たった今ではこの枠組みを維持することが一番本質的なことだったと感じています。

モバイルファースト室ではAndroid、iOS各チームでリリースごとに約2週間に1回程度の頻度でKPTを用いた振り返りを実施しています。

最初のステップはルール作り

1回目の振り返りはProblemが大量にありKeepは見つからないという傾向になりがちなので、ちょっとしたことでもKeepに挙げたり、Problemで人を責めないようにすることなどを注意しながら始めました。

またこの段階での一番の課題は無法地帯だった開発プロセスをコントロールできるルールを作ることでした。

そこで最初のTryでは開発開始からリリースまでのルールを決定しました。

具体的には

  • 約2週間に1回のリリースサイクルを決め、開発チームはリリース希望日に一番近いリリース日を選択してもらう方式に
  • コードフリーズ日を決め、それ以降の機能追加を禁止
  • リリースバージョンごとにGitHubのMilestoneを作成し、そのバージョンに含める開発項目をissueにして管理
  • テスト期間を定めissueに含まれる開発項目を漏れ無くテスト

といったルールを決めました。 これにより、部署間でのスケジュール調整が不要になる、直前の駆け込みコミットを禁止できるようになるなど、開発上のリスク要因を理論上大幅に削減することができるようになりました。

次はルールを徹底する負担の削減

ルールを決めて何度かイテレーションを繰り返しルールの精度を上げていくと、次はそのルールを維持するコストが問題になりました。忙しい日々の中で常に様々なルールを意識して行動するのは意外に難しいものです。そのため気づくとコードフリーズに間に合わない状況が発生していたり、リリース準備のための細々とした準備作業のタスク漏れによる作業遅延などの問題が起きていました。

そこで次の振り返りではルールの運用コストを下げる施策をTryしました。

具体的には開発開始からリリースまでに必要な作業をチェックリストにして1つのissueにまとめるというものです。

Image may be NSFW.
Clik here to view.
f:id:y_310:20141029224237p:plain

これによって作業を記憶しておく必要がなくなり負担が大きく減りました。 このチェックリストを使う試みはシンプルながら思いの外うまくいったので、以前こちらのブログで紹介した アプリ開発の品質底上げ施策をWebhooksでBotが支援する世界においても、Pull Requestマージ前の確認項目として使っています。

誰がルールを運用するのか

チェックリストを作って運用を始めたところ、誰がこの作業をするのかというのが次の問題になりました。手が空いた人が率先してやるという形でやっていましたが、自分のタスクに集中しているうちに気づくと誰もやっていないという状況が起きていました。

そこで次の振り返りではリリースマネージャーというそのバージョンのリリースに責任を持って取り組む役割を設定しました。 リリースマネージャーはそのバージョンに含める開発項目の決定や、開発途中でのタスクの進捗確認、コードフリーズやリリース作業などリリースを成功させるための作業全体に責任を持ちます。 バージョンごとにリリースマネージャーの担当者を変更することで特定の人に負荷が偏らないようにしながら運用しています。

そしてその先へ

この他にも細かい改善を沢山積み上げていますが、ここまでの取り組みで開発プロセスをほぼコントロールできるようになりました。 結果として深刻なバグや大幅なスケジュール遅延を起こすことなく、半年間でAndroid、iOSそれぞれ10回ずつリリースすることができました。

ただ今までの取り組みはほぼ問題を起こさないための仕組みづくりでした。 ここで一定の成果が出たため、今後はユーザ価値の観点で見た品質向上にシフトしていこうとしています。 これについてはいつか良い成果が出てきたらまたご紹介したいと思います。

KPTの重要性

今回の取り組みにおいて、KPTによって着実な一歩を積み重ねていく仕組みを作ることは個々の施策以上に重要なことだと考えています。

プロセス改善はそれ自体が目的ではないため、散発的に改善のアイディアを出しても継続できず自然消滅してしまうことがよく起こります。 そこにKPTを導入することで、失敗した施策から目を背けず、成功するまでTryを繰り返す仕組みづくりができるようになります。

KPTのコツ

KPTを実践する中で見えてきたコツを箇条書きですがいくつかご紹介します。

  • 前回のTが実践できていたかどうか必ず最初に確認する
    • やらないと改善策を出しただけで実践されないままになる
  • Kは少しでも良いと感じたら躊躇わずに挙げて改善の取り組みがうまく行っているという雰囲気を出す
    • 最初のうちは「振り返りを実施できた」というレベルでも重要なKになる
  • Tが浮かばない場合Pが抽象的過ぎる可能性が高いので、問題を一般化せず具体的に捉えなおす
    • 「バグが多い」というPならどういうケースのバグが多いのか、開発のどの段階で見つけられるのかといった詳細を分析してTを出す
  • Pが多い場合はすべてを一度に解決しようとはせず、着実に現状より改善出来る範囲のTを実践する
    • 一回のイテレーションでできることは限られていると自覚して確実にできる施策だけをTに含める
  • Tは「気をつける」「注意する」といった心がけではなく、チェックリストを作れるような具体的な行動にする
    • 次回の振り返りで出来たかどうか明確に判定できないものはTではない

最後に

今回ご紹介した取り組みは目の醒めるようなアイディアや先進的な技術といったものは全く登場しない地道なものです。

もちろん技術で解決できることはどんどん取り組んでいきますが、それだけに固執せずに小さい問題解決を積み重ねることにも価値があるということがお伝えできればと思います。

RESTful Web API 開発をささえる Garage

技術部の小野(@taiki45)です。この記事では簡単なアプリケーション(ブログシステム)の実装を通して、クックパッドで作成・使用しているライブラリのGarageの紹介と Garage を使った RESTful Web API の開発をご紹介したいと思います。

Garage は RESTful Web API を開発するための、 Rails gemified pluginsです。Rails プログラマは Garage を使って Rails を拡張することで素早く Web API を開発することができます。Garage は新しくアプリケーションを開発する場合にも、既存の Rails アプリケーションに組み込んで Web API を実装する場合でも使用できます。Garage はリソースのシリアライズやアクセスコントロールなど Web API の実装に必要な機能をカバーしています。

RubyKaigi2014 にて Garage の OSS 化をお知らせしましたが、実際のアプリケーション開発向けの情報が少ないので、この記事とサンプルアプリケーションを通じて補完したいと思います。

この記事で実装するブログアプリケーションのコードは https://github.com/taiki45/garage-exampleにあります。

今回実装するアプリケーション

次のようなブログシステムを実装します。

  • アプリケーションが提供するリソースはログインユーザーである user と投稿された投稿である post の2つ。
  • user について以下の操作を提供します
    • ユーザーの一覧の表示 GET /v1/users
    • それぞれのユーザーの情報の表示 GET /v1/users/:user_id
    • 自身の情報の更新 PUT /v1/users/:user_id
  • post については以下の操作を提供します。
    • 新規記事の作成 POST /v1/posts
    • アプリケーション全体の記事の一覧の表示 GET /v1/posts
    • あるユーザーの投稿した記事一覧の表示 GET /v1/users/:user_id/posts
    • それぞれの記事の情報の表示 GET /v1/posts/:post_id
    • 自身の投稿した記事の更新 PUT /v1/posts/:post_id
    • 投稿した記事の削除 DELETE /v1/posts/:post_id
  • user の作成や削除については実装しません。

実際の開発だとクライアントアプリケーションの API 利用の仕方によって、リソースの URL 表現やリソースの関係の表現方法(リソースを埋め込みでレスポンスするかハイパーリンクとしてレスポンスするか)は異なります。今回はこのように設計します。

Rails new

それでは実際にブログアプリケーションを実装していきます。Garage は Rails の gemified plugin なのでアプリケーションの作成について変更する点はありません。

Garage アプリケーションの開発には典型的には RSpec を使用するので、ここでは --skip-testunit フラグを付けて rails newコマンドを実行します。

❯ rails new blog --skip-bundle --skip-test-unit -q && cd blog

開発に必要な gem を追加します。現在 rubygems.org でホストされている garage gemはこの記事で扱っている Garage とは別の gem ですので、Bundler の github shorthand を使って Garage を指定します。

# Gemfile+gem 'garage', github: 'cookpad/garage'++group :development, :test do+  gem 'factory_girl_rails', '~> 4.5.0'+  gem 'pry-rails', '~> 0.3.2'+  gem 'rspec-rails', '~> 3.1.0'+end+

bundle install と rspec helper の設定を行っておきます。

❯ bundle install
❯ bundle exec rails g rspec:install
# spec/rails_helper.rb-# Dir[Rails.root.join("spec/support/**/*.rb")].each { |f| require f }+Dir[Rails.root.join("spec/support/**/*.rb")].each { |f| require f }# spec/spec_helper.rb-# The settings below are suggested to provide a good initial experience-# with RSpec, but feel free to customize to your heart's content.-=begin
   # These two settings work together to allow you to limit a spec run
   # to individual examples or groups you care about by tagging them with
   # `:focus` metadata. When nothing is tagged with `:focus`, all examples
@@ -81,5 +78,4 @@ RSpec.configure do |config|
   # test failures related to randomization by passing the same `--seed` value
   # as the one that triggered the failure.
   Kernel.srand config.seed
-=end
 end

# spec/support/factory_girl.rb+RSpec.configure do |config|+  config.include FactoryGirl::Syntax::Methods+end

Configuration and authentication/authorization

bundle install や rspec helper の設定を終えた後は、Garage 初期設定と認可ライブラリの Doorkeeperの初期設定をします。

Doorkeeper は Rails アプリケーションに OAuth 2 provider の機能を持たせるための gem です。Garage は Doorkeeper を用いて認可機能を提供します。

クックパッドでは複数の Garage アプリケーションが存在しているので、Doorkeeper を使用せずに、認証認可サーバーへ認証認可を委譲するモジュールを Garage に追加しています。このような Doorkeeper 以外の認可実装や認可を行わない実装への対応は Garage 本体へ追加予定です。

今回実装するアプリケーションでは Doorkeeper を用いて1アプリケーション内で認証認可を完結させます。config/initializers/garage.rb を作成し、Garage と Doorkeeper の設定を追加します。

# config/initializers/garage.rb+Garage.configure {}+Garage::TokenScope.configure {}++Doorkeeper.configure do+  orm :active_record+  default_scopes :public+  optional_scopes(*Garage::TokenScope.optional_scopes)++  resource_owner_from_credentials do |routes|+    User.find_by(email: params[:username])+  end+end# config/routes.rb
 Rails.application.routes.draw do
+  use_doorkeeper
 end

Garage の設定の骨組みと今回のアプリケーション用の Doorkeeper の設定を追加しています。resource_owner_from_credentialsメソッドで設定しているのは OAuth2 の Resource Owner Password Credentials Grant を使用した時のユーザーの認証方法です。今回は簡単にするため、パスワード無しでメールアドレスのみを用いて認証を行います。

設定を定義した後は、Doorkeeper が提供する migration を生成して実行しておきます。

❯ bundle exec rails generate doorkeeper:migration
❯ bundle exec rake db:create db:migrate

Start with GET /v1/users

まずはコントローラーやモデルの作成を行って、ユーザー情報の GET ができるところまで進めます。

コントローラーの作成

Rails 標準の ApplicationControllerあるいはすでに実装されている Rails アプリケーションに Garage を使用する場合はそれに準ずる抽象コントローラークラス(ApiControllerなど)に Garage::ControllerHelperを include します。ControllerHelperは全てのコントローラーに共通の基本的なフィルタとメソッドを提供します。Doorkeeper を使用した認証と認可も行われます。

# app/controllers/application_controller.rb++  include Garage::ControllerHelper++  def current_resource_owner+    @current_resource_owner ||= User.find(resource_owner_id) if resource_owner_id+  end
 end

Garage の規約としてここでユーザーが定義すべきものは current_resource_ownerというメソッドです。このメソッドはリクエストされたアクセストークンに紐付いているリソースオーナー情報を使用してアプリケーションのユーザーオブジェクトへ変換することが期待されています。注意する点としては、OAuth2 の client credentials などの grant type で認可したアクセストークンについてはリソースオーナー情報が紐つかないので nil が入っていることがあります。ここで変換したユーザーオブジェクトに対して後述するアクセスコントロールロジックが実行されます。

次に普段の Rails アプリケーションと同じような命名規則でユーザーリソースの提供用に UsersController を作成します。routes 設定は普段の Rails アプリケーションと同じです。

# app/controllers/users_controller.rb+class UsersController < ApplicationController+  include Garage::RestfulActions++  def require_resources+    @resources = User.all+  end+end# config/routes.rb
 Rails.application.routes.draw do
   use_doorkeeper
++  scope :v1 do+    resources :users, only: %i(index show update)+  end
 end

リソースを提供するコントローラーでは Garage::RestfulActionsを include して、index/create/show/update/delete それぞれに対応する require_resources/create_resource/require_resource/update_resource/destroy_resource メソッドを定義します。ここでは index に対応する require_resourcesメソッドを定義しています。Garage::RestfulActionsがユーザー定義の require_resourcesなどを使用して実際の action をラップした形で定義してくれます。

ここでは紹介しませんでしたが、他にも Garage はページネーション機能などを提供しています。コントローラーで respond_with_resources_optionsメソッドをオーバーライドして paginate option を有効にすることで、リソースコレクションの総数や次のページへのリンクなどをレスポンスすることができます。サンプルアプリケーションでは実装しているので、ぜひご覧になってください。

モデルとリソースの定義

ActiveRecord モデルは Rails 標準のやり方で作成します。今回のアプリケーションではユーザーは自由に設定できる名前と認証用の email アドレスを持つことにします。

❯ bundle exec rails g model user name:string email:string
❯ bundle exec rake db:migrate

モデルにリソースとしての定義を追加します。Garage はこの定義を利用してリソースのシリアライゼーションやアクセスコントロールを実行します。

# app/models/user.rb
 class User < ActiveRecord::Base
+  include Garage::Representer+  include Garage::Authorizable++  property :id+  property :name+  property :email++  def self.build_permissions(perms, other, target)+    perms.permits! :read+  end++  def build_permissions(perms, other)+    perms.permits! :read+    perms.permits! :write+  end
 end

# config/initializers/garage.rb
 Garage.configure {}
-Garage::TokenScope.configure {}+Garage::TokenScope.configure do+  register :public, desc: 'acessing publicly available data' do+    access :read, User+    access :write, User+  end+end

 Doorkeeper.configure do

Garage::Representerがリソースのエンコーディング・シリアライゼーションを提供するモジュールです。propertyでリソースが持つ属性を宣言します。他にも linkを用いて他のリソースへのリンクを宣言することもできます。詳しくはサンプルアプリケーションを参照してください。

Garage::Authorizableがアクセスコントロール機能を提供するモジュールです。アクセスコントロールについては後述するので、ここではパブリックなリソースとして定義をしておきます。同様に OAuth2 のスコープによるアクセスコントロールについても後述するのでここではパブリックな定義にしておきます。

ローカルサーバーでリクエストを試す

ここまででユーザーリソースの GET を実装したのでローカル環境で実行してみます。

# テストユーザーを作成します
❯ bundle exec rails runner 'User.create(name: "alice", email: "alice@example.com")'
❯ bundle exec rails s

サーバーが起動したら Doorkeeper が提供する OAuth provider の機能を利用してアクセストークンを取得します。http://localhost:3000/oauth/applicationsを開いてテスト用に OAuth クライアントを作成して client id と client secret を作成して、アクセストークンを発行します。

❯ curl -u "$APPLICTION_ID:$APPLICATION_SECRET" -XPOST http://localhost:3000/oauth/token -d 'grant_type=password&username=alice@example.com'

{"access_token":"XXXX","token_type":"bearer","expires_in":7200,"scope":"public"}

取得したアクセストークンを使って先ほど実装したユーザーリソースを取得します。

❯ curl -s -XGET -H "Authorization: Bearer XXXX" http://localhost:3000/v1/users | jq '.'

[
  {
    "id": 1,
    "name": "alice",
    "email": "alice@example.com"
  }
]

You're done!!

自動テスト

Garage アプリケーションを開発する上で自動テストをセットアップするまでにいくつか注意する点があるので、ここでは request spec を実行するまでのセットアップを紹介します。

Doorkeeper による認証認可をスタブするためのヘルパーを追加します。

# spec/support/request_helper.rb+require 'active_support/concern'++module RequestHelper+  extend ActiveSupport::Concern++  included do++    let(:params) { {} }++    let(:env) do+      {+        accept: 'application/json',+        authorization: authorization_header_value+      }+    end++    let(:authorization_header_value) { "Bearer #{access_token.token}" }++    let(:access_token) do+      FactoryGirl.create(+        :access_token,+        resource_owner_id: resource_owner.id,+        scopes: scopes,+        application: application+      )+    end++    let(:resource_owner) { FactoryGirl.create(:user) }+    let(:scopes) { 'public' }+    let(:application) { FactoryGirl.create(:application) }+  end+end

RSpec example group の中で必要に応じて resource_ownerscopesを上書き定義することで、リソースオーナーの違いや OAuth2 のスコープの違いを作りだせます。

ついでに細かいところですが、facotry の定義を書き換えておきます。

# spec/factories/users.rb
 FactoryGirl.define do
   factory :user do
-    name "MyString"-email "MyString"+    sequence(:name) {|n| "user#{n}" }+    email { "#{name}@example.com" }
   end
-
 end

最初の request spec は最小限のテストのみ実行するようにします。

# spec/requests/users_spec.rb+require 'rails_helper'++RSpec.describe 'users', type: :request do+  include RequestHelper++  describe 'GET /v1/users' do+    let!(:users) { create_list(:user, 3) }++    it 'returns user resources' do+      get '/v1/users', params, env+      expect(response).to have_http_status(200)+    end+  end+end

テスト用データベースを作成してテスト実行してみます。

❯ RAILS_ENV=test bundle exec rake db:create migrate
❯ bundle exec rspec -fp spec/requests/users_spec.rb

Run options: include {:focus=>true}

All examples were filtered out; ignoring {:focus=>true}
.

Finished in 0.06393 seconds (files took 1.67 seconds to load)
1 example, 0 failures

無事自動テストのセットアップができました。

よりテストコードを DRY にするには rspec-request_describer gem を導入することもおすすめです。

リソースの保護

実際の Web API ではリソースに対するアクセスコントロールや権限設定が重要になります。具体的には、ユーザーが認可したクライアントにのみプライベートなメッセージの読み書きを許可したり、あるいはブログ記事の編集は投稿者自身にのみ許可する、といった例があります。

Garage では OAuth2 利用したアクセス権の設定をリソース毎に定義することと、リクエストコンテキストを使用してリソース操作に対する権限をリソース毎に定義することで、リソースの保護を実現できます。

アクセスコントロールについては概念自体わかりにくいと思うので、実際に動くアプリケーションで試してみます。サンプルアプリケーションアプリケーションのリビジョン 1b3e35463b87631d1e6acdd08e11ae09cab1b7ccをチェックアウトします。

git clone git@github.com:taiki45/garage-example.git && cd garage-example
git checkout 1b3e35463b87631d1e6acdd08e11ae09cab1b7cc

ここではユーザーリソースに対してリソース操作の権限設定をしてみます。他人のユーザー情報は閲覧できる、他人のユーザー情報は変更できない、という挙動に変更します。テストとしては次のように書けます。

# spec/requests/users_spec.rb
  describe 'PUT /v1/users/:user_id'do
    before { params[:name] = 'bob' }

    context 'with owned resource'do
      let!(:user) { resource_owner }

      it 'updates user resource'do
        put "/v1/users/#{user.id}", params, env
        expect(response).to have_http_status(204)
      endend

    context 'without owned resource'do
      let!(:other) { create(:user, name: 'raymonde') }

      it 'returns 403'do
        put "/v1/users/#{other.id}", params, env
        expect(response).to have_http_status(403)
      endendend

テストが失敗することを確かめます。

❯ bundle exec rspec spec/requests/users_spec.rb:24

users
  PUT /v1/users/:user_id
    with owned resource
      updates user resource
    without owned resource
      returns 403 (FAILED - 1)

ユーザーリソースのパーミッション組み立てロジックを変更します。otherはリクエストにおけるリソースオーナーが束縛されます。リソースオーナーは先ほど ApplicationController で実装した current_resource_ownerメソッドで変換されたアプリケーションのユーザーオブジェクトが束縛されているので、今回のアプリケーションだと User クラスのインスタンスです。

# app/models/user.rb
   def build_permissions(perms, other)
     perms.permits! :read
-    perms.permits! :write+    perms.permits! :write if self == other
   end

テストを実行してみます。

❯ bundle exec rspec spec/requests/users_spec.rb:24

users
  PUT /v1/users/:user_id
    without owned resource
      returns 403
    with owned resource
      updates user resource

他人のユーザー情報は更新できないようにできました。

Garage は他にもブログの下書き投稿は投稿者しか閲覧できない、ユーザーの名前の変更は特定のスコープがないと変更できない、など様々な権限設定ができます。ここでは紹介しませんでしたが、アクセス権の設定やいくつかの拡張機能についてはサンプルアプリケーションとドキュメントを参照してください。

Response matcher

JSON API のレスポンスのテストは RSpec2 では rspec-json_matcherを用いて、RSpec3 では composing-matchers を使用して記述します。テストによっては構造を検査するだけでなく、実際のレスポンスされた値を検査します。

RSpec2

  let(:post_structure) do
    {
      'id' => Integer,
      'title' => String,
      'body' => String,
      'published_at' => String
    }
  end

  describe 'GET /v1/posts/:post_id'do
    let!(:post) { create(:post, user: resource_owner) }

    it 'returns post resource'do
      get "/v1/posts/#{post.id}", params, env
      response.status.should == 200
      response.body.should be_json_as(post_structure)
    endend

RSpec3

  let(:post_structure) do
    {
      'id' => a_kind_of(Integer),
      'title' => a_kind_of(String),
      'body' => a_kind_of(String).or(a_nil_value),
      'published_at' => a_kind_of(String).or(a_nil_value)
    }
  end

  describe 'GET /v1/posts/:post_id'do
    let!(:post) { create(:post, user: resource_owner) }

    it 'returns post resource'do
      get "/v1/posts/#{post.id}", params, env
      expect(response).to have_http_status(200)
      expect(JSON(response.body)).to match(post_structure)
    endend

DebugExceptions

Rails はデフォルトの設定だと development 環境ではサーバーエラーが起きた場合、 ActionDispatch::DebugExceptionsがエラー情報を HTML でレスポンスします。JSON API 開発の文脈ではデバッグ用のエラーレスポンスも JSON のほうが都合が良いです。その場合 debug_exceptions_json gem を使います。エラー情報が JSON でレスポンスされるので、開発がしやすくなります。また、RSpec との連携機能があり、request spec の実行中にサーバーエラーが起きると RSpec のフォーマッタを利用してエラー情報をダンプしてくれます。

Failures:

  1) server error dump when client accepts application/json with exception raised responses error json
     Failure/Error: expect(response).to have_http_status(200)
       expected the response to have status code 200 but it was 500
     # ./spec/features/server_error_dump_spec.rb:21:in `block (4 levels) in <top (required)>'

     ServerErrorDump:
       exception class:
         HelloController::TestError
       message:
         test error
       short_backtrace:
         <backtrace is here>

APIドキュメント

Web API 開発の文脈では、API のドキュメントを提供しドキュメントを最新の状態にアップデートしておくことで開発中のコミュニケーションを効率化できます。

API ドキュメントの生成には autodoc gem を使っています。リクエスト例、レスポンス例だけでなく、weak_parameters gem と組み合わせることでリクエストパラメータについてもドキュメント化できます。生成されたドキュメントは Garage のドキュメント提供機能を使用してブラウザで閲覧できるようにすることもできますし、より簡単には markdown フォーマットで生成されるので Github 上でレンダーされたドキュメントを参照してもらうこともできます。

Image may be NSFW.
Clik here to view.
f:id:aladhi:20141105172010p:plain


Garage を使用した RESTful Web API の開発についてご紹介しました。コントローラーを作る、リソースを定義する、アクセスコントロールを定義する、このステップを繰り返すことでアプリケーションを開発することができます。

この記事が Garage を使用したアプリケーション実装の参考になれば幸いです。

aptly による apt リポジトリ管理

インフラストラクチャー部の宮下(@gosukenator)です。

クックパッドでは一部のサーバで Ubuntu を使い始めており、 apt リポジトリをどのように管理するのが良いのか、試行錯誤しています。aptリポジトリ管理で実現したいことは、主に次の2点です。

  • 自前でビルドしたパッケージの管理
  • リモートリポジトリから削除された旧バージョンパッケージの保全

このあたりをいい感じにできるツールはないかな、と社内で話していたところ、カルビ生焼け王に教えてもらったのが aptlyです。

aptly とは

公式サイトに「aptly is a swiss army knife for Debian repository management」とあるように、aptly は多機能な apt リポジトリ管理用ツールです。外部リポジトリのミラー作成、ローカルリポジトリの作成、リポジトリのスナップショット作成、スナップショット同士のマージ、S3 への publish 等の機能があります。「swiss army knife」と謳ってるのは伊達ではなく、かなり多機能でツールの全体像がつかみにくいのですが、オフィシャルサイトの Overviewにわかりやすい図などがあるので、そちらを参照してください。

詳しい使い方等はここでは解説しませんが、興味のある方は Tutorialからトライしてみると良いでしょう。

aptly でやろうとしていること

現在 aptly を利用して、以下のことを実現しようとしています。

  • 自前パッケージ管理用のローカルリポジトリの運用
  • リモートリポジトリのミラー作成と、スナップショットを毎日保存してリモートから削除されたパッケージを保全
  • ローカルリポジトリとリモートミラーのスナップショットをマージして S3 へ publish

図にすると以下のようなイメージです。

Image may be NSFW.
Clik here to view.
f:id:MIZZY:20141106143626p:plain

ローカルリポジトリの運用

ローカルリポジトリは次のように作成します。

$ aptly repo create cookpad-repo

ローカルリポジトリにパッケージを追加するには次のように実行します。

$ aptly repo add cookpad-repo package.deb

リモートリミラー作成とスナップショット作成

リモートリポジトリのミラーは次のように作成します。

$ aptly mirror create -architectures=amd64 ubuntu-mirror \
  http://ap-northeast-1.ec2.archive.ubuntu.com/ubuntu/ trusty

リモートリポジトリとミラーを同期するには、次のように実行します。

$ aptly mirror update ubuntu-mirror

aptly mirror updateを実行すると、リモートリポジトリと完全に同期されるため、リモートで削除されたパッケージはミラーでも削除されてしまいます。リモートで削除されても手元に残るよう、スナップショットを作成します。

$ aptly snapshot create ubuntu-mirror-20141106 from mirror ubuntu-mirror

毎日スナップショットを取得することで、リモートで削除されたパッケージの保全を行います。

ローカルリポジトリとリモートミラーのマージ

ローカルリポジトリとリモートミラーをマージするために、ローカルリポジトリのスナップショットを取得します。

$ aptly snapshot create cookpad-repo-20141106 from repo cookpad-repo

ローカルリポジトリのスナップショットと、リモートミラーのスナップショットをマージしたスナップショットを作成します。リモートで削除されたパッケージをすべて含めるため、リモートミラーから取得したスナップショットすべてをマージ対象に含めます。ローカルリポジトリのパッケージは基本的に削除しないので、最新のスナップショットのみマージ対象に含めます。

aptly snapshot merge --no-remove merged-snapshot-20141106 cookpad-repo-20141106 \
  ubuntu-mirror-20141106 ubuntu-mirror-20141105 ubuntu-mirror-20141104

マージしたスナップショットを S3 へ publish

マージしたスナップショットを publish します。初めて publish する場合はaptly publish snapshotを実行します。

$ aptly publish snapshot merged-snapshot-20141106 s3:cookpad:

2度目以降は aptly publish switchを実行します。

$ aptly publish switch trusty s3:cookpad: merged-snapshot-20141106

問題点

この方法の問題点は、publish switch実行時に、差分ではなくすべてのパッケージをアップロードするため、非常に時間がかかる、ということです。特に、Ubuntu のパッケージアーカイブをすべてミラーしていると、4万個以上のパッケージをアップロードすることになり、EC2インスタンスからS3への転送に2時間以上時間がかかります。しかも、途中でネットワークエラー等で中断されると、最初からやり直しになってしまいます。

したがって、この方法は現実的でないため、他の方法を模索中です。私が aptly の機能をすべて把握できておらず、他にいい方法があるのに見つけられていない可能性もあるため、現在ドキュメントを読み漁っています。aptly がまだバージョン 0.8 なので、今後のバージョンアップでいい感じに解決できるようになるかもしれません。

もし他にいい方法やいいツールなどありましたら、ぜひ教えてください。

デザイン設計に集中する時間を増やしてみよう

こんにちは! ユーザーファースト推進部のデザイングループのジョン・ジンホ(@img75)です。

前回、クックパッドのデザインプロセスについてご紹介しましたが、私からはクックパッドのデザインプロセスをより効率的にまわす為に、デザイナーとしてどのようなツールを活用しているのかを、今回は Adobe Photoshop CC のプラグインを中心にご紹介したいと思います。

デザイン設計に集中できる

Photoshop などでの作業効率を向上させると、デザイナーにとって貴重な「デザイン設計に集中できる時間」が生まれます。それは結果的にクックパッドを利用してくださるみなさまにより良い機能をすばやく提供できることにつながるので、私はいつもデザインするにあたって不必要な時間を減らす努力をしています。

最近、私は「撮るレシピ」(Android/ SPweb版)のデザインを担当しました。 「撮るレシピ」は、このレシピを覚えておきたいな、と思ったら写真に撮って簡単に保存、いろいろなところにあるレシピを一箇所に集めて見ることができるサービスです。ぜひ試してみてください。

このプロジェクトを進めるにあたって、さまざまな Photoshop プラグインを活用しました。ここではそのプラグインを以下の2つのカテゴリにわけてご紹介させていただきます。

  1. デザイン作業を手助けしてくれるプラグイン
  2. 書き出し、ガイド作成を手助けしてくれるプラグイン

デザイン作業を手助けしてくれるプラグイン

設計作業を支援するプラグインは様々ですが、今回のプロジェクトでは、私は以下のような2つのプラグインを多く使用しています。プロジェクトを進行しながら、どのように使用したのかを説明したいと思います。

ガイドを簡単に設定出来る​​

ウェブもそうですが、アプリを設計する際にも、それぞれの基本的な領域があります。インジケータ領域、アクションバー領域がそうです。私はこれらの基本的な領域に加え、各レイヤーを整列するためにキャンバスにガイドをたくさんかきます。しかしガイドをたくさんかくのは大変でそれなりに時間がかかってしまいます。その時間を減らすために Guideguideというプラグインを使っています。

Image may be NSFW.
Clik here to view.
f:id:img75:20141107161332p:plain

このプラグインでよく使うのは Grid notation機能です。これはガイドをセットとしてまとめることができる機能です。私の場合は上の画像のように 基本的なインジケーター領域とアクションバーの領域をあらかじめ設定したものをセットとして用意して、すぐデザインをはじめられるようにしています。クックパッドではたくさんのアプリを開発していますが、各アプリでトーンやマナーを合わせるする意味でもこのようなプラグインを使って領域を整えやすくすることはとても重要だと感じています。

アイコンをすばやく検索、適用する

アイコン素材を PSD や AI ファイルに保存して、そこから用途に合わせて使うようにしているデザイナーも多いと思いますが、私はこういう場面でもプラグインを活用して時間の短縮を実践しています。

Image may be NSFW.
Clik here to view.
f:id:img75:20141107161402p:plain

Flaticonはフリーライセンスのアイコン素材を検索することができるプラグインです。データが非常に膨大で、Photoshop のシェイプとして使用することができるようになっています。実際に使うアイコンとは少し違いますが、すばやくイメージを固めていくためのデザインプランを素早く練っていけるという利点があります。「撮るレシピ」のプロトタイピングでも実際に使用したアイコンは新たに作ったものですが、それまでのデザインに落し込んでいくプロセスでイメージを固めていくのに役立ちました。たとえば、「Search」というキーワードを入力すると、様々なスタイルのアイコンが表示されるので、そこからイメージに近いものを選んでさっと試してみることができます。

書き出し、ガイド作成を手助けしてくれるプラグイン

デザインを固めたらそれをエンジニアに伝えるためにデザインガイドの作成します。デザインガイドは、テキストやボタンの大きさ、色などを詳しく説明する必要があり、これを作成するのにもやはり時間がかかってしまいます。できるだけシンプルで素早く作成するためにも私はいくつものプラグインを活用しています。

使用したフォント、ボタンのサイズや余白を表示​​

私はガイドの作成にあたって、主にAssistor PSというプラグインを使ってデザインガイドを作成しています。上の画像のようにガイドボックス、フォントのサイズや色、種類、およびさまざまなガイドを作成することができます。

Image may be NSFW.
Clik here to view.
f:id:img75:20141107161443p:plain

(撮るレシピのガイド)

Image may be NSFW.
Clik here to view.
f:id:img75:20141107161457p:plain

(撮るレシピのActionBar)

デザインガイド文書は、UIデザイナーにとって非常に時間がかかる作業ですが、私の場合には、上記のデザインガイド文書を一日程度の時間で完了することができました。

コードで表現が可能な部分は、コードに渡す

Image may be NSFW.
Clik here to view.
f:id:img75:20141107161622p:plain

「撮るレシピ」の場合は、ボタンなどの部分について、画像ファイルを使用せずに、すべてコードで処理しました。コードで処理すると、アプリの全体的な容量を減らすことができるほか、読み込み速度も速くなります。これは画面ザイズが多様な Android 環境に対応するためにも重要です。これらのコードのエクスポート機能は、CSS3psを利用して素早く簡単に作成しています。

9パッチを簡単に作る

Androidでは内容に応じて拡大縮小可能なグラフィックとして、「9 Patch」というグラフィックを扱うことができます。この9パッチグラフィックを理解していないと Android の UI デザインをすることが大変かと思います。どこまで拡張が必要な領域であることを考えて1ピクセル、1ピクセル描くのが時間がかかる作業なので、その作業を解決してくれる Clear Nine Patchというプラグインを利用してみるのはいかがでしょうか?

Image may be NSFW.
Clik here to view.
f:id:img75:20141107161643p:plain

「撮るレシピ」のActionBarでは、グラデーションを使用したグラフィックがあったので、9パッチで処理しました。ボタン1つで、9パッチ処理した画像を作成することができます。これで、Android の様々な画面に対応した、画像を作成することができました。

Tip! Photoshopの初期設定

プラグインと合わせて、私が用意しているPhotoshop の初期設定をご紹介します。初期設定を用意しておくことで、毎回画面サイズを確認しなくても、素早くデザインをはじめられます。

Image may be NSFW.
Clik here to view.
f:id:img75:20141107161700p:plain

まとめ

さて、いかがでしたでしょうか?クックパッドのデザイングループでは、クックパッドをご利用の方に、より迅速にサービスをご提供するため、ツールにも気を配っています。ツールを使うのは、デバイスが多様化していて書き出す画像のサイズを複数用意したり、9パッチのように特殊な画像を作成しなければならないなど、デザイナーがただただデザインをして終わりではなく、デザインをした後にエンジニアが迅速に作業を行えるようにデザイナーが考えなければないかと思います。

この他にも便利なプラグインなどは多くあるので、「こんなプラグインを利用してるよ!」というのがあればぜひ教えてください。今回のブログを通じて、より便利なツールがデザイナーに広まることができれば良いと思います。

CocoaPods Private SpecsでiOS用社内ライブラリを管理する

技術部のid:gfxです。iOSアプリ開発に欠かせないパッケージ管理ツールといえばCocoaPodsですが、これはPrivate Podsを作って社内ライブラリ専用のSpecs(private Specs)を管理することができます。

private SpecsがなくてもGit URLを指定することで社内用podspecを開発・管理することはできますが、private SpecsがあるとURL指定を簡略化したり依存関係の解決が簡単になるというメリットがあります。クックパッドでもすでに十数個のprivate podspecが登録されており、CookpadUIやAPI clientなどはpodspecとしてprivate Specsに登録されています。

そこで本エントリでは、Specsを運用している間に起きた問題などを共有します。なお、CocoaPodsのバージョンは v0.35.0.rc2 です。v0.34.xだとGit SSH URLを使う際にトラブルがあったのでRC版を使っていますが、インストールの際に --preが必要なので注意してください。また、private Specsの仕様はまだ流動的なので、実際に運用に入る際は仕様をご確認ください。

概要

  • podspecでGit SSH URLを参照しているときは pod lib lint--only-errorsをつける
  • pod repo pushには --allow-warningsをつける
  • podspecでprivate Specsに依存する場合、pod lib lintには `--sources=...'でprivate SpecsのURLを与える

詳細

まずCocoaPods Specsの実体ですが、これはただの特別なパス構造を持ったgit repositoryです。仕様に従ったgit repositoryを用意すれば、検索こそできませんがPodfileでデフォルトのCocoaPods/Specs同様に使えます。

なお、本エントリでは以下の3つのgit repositoryを使います。

また、以下のようにprivate Specsを登録しておきます。PrivateSpecsExampleは空でかまいません。git repositoryの構成についてはpodコマンドがすべて面倒をみてくれるので、気にする必要がないのです。

$ pod repo add myspecs git@github.com:gfx/PrivateSpecsExample.git

podspecを作る

それでは、podspecをひとつ作り、private Specsに登録してみましょう。podspecのリポジトリはExamplePodです。

podspecは pod spec create ExamplePodで作り、生成されたExamplePodを修正します。 s.sourceはGitHub Enterpriseを想定しているのでGit SSH URLで指定しています。

-  s.source       = { :git => "http://EXAMPLE/ExamplePod.git", :tag => "0.0.1" }+  s.source       = { :git => "git@github.com:gfx/ExamplePod.git", :tag => "0.0.1" }

ここでポイントその一ですが、pod lib lintは Git SSH URLだとfirewall越しにアクセスできない可能性があるというwarningsを出します 。そして、 pod lib lintはデフォルトでwarningsを致命的エラー扱いにします。errorのみを致命的エラーにするには、 --only-errorsオプションを指定しなければいけません。

このオプションのもとでの結果は以下のようになります。warningsは出ていますがvalidationはパスしています。

$  pod lib lint --only-errors

 -> ExamplePod (0.0.1)
    - WARN  | [source] Git SSH URLs will NOT work for people behind firewalls configured to only allow HTTP, therefore HTTPS is preferred.

ExamplePod passed validation.

なお、ここではやむを得ずwarningsを無視していますが、基本的にはwarningsを無視するべきではありません。他のwarningsが出ていないかの確認はするほうがいいでしょう。

podspecをpushする

次に、これをprivate Specsにpushしてみます。今回は同じrepositoryを使いますが、実際には社内のSpecsに対して行います。この時、前述の理由でGit SSH URLだとspec validationに失敗するのですが、このとき必要なオプションは--allow-warningsです。これが第二のポイントです。

# tagを打ってpodspecをpushする
$ git tag 0.0.1 && git push origin 0.0.1
$ pod repo push --allow-warnings  myspecs *.podspec

これでpodspecをprivate Specsに登録できました。あとはこのpodspecを使いたいプロジェクトのPodfileで以下のように sourceを指定すれば、そこに登録されたpodspecを使うことができます。

# Podfile
source 'git@github.com:gfx/PrivateSpecsExample.git'
source 'https://github.com/CocoaPods/Specs.git'

pod 'ExamplePod'

private podspecに依存したpodspecを作る

さて、このprivate podspecに更に依存したpodspecを作ってみます。内容はExamplePodと同じのExamplePod2をつくり、podspecの名前やパスを書き換えます。また、 s.dependency 'ExamplePod'を加えてprivate podspecへの依存を宣言します。

# ExamplePod2.podspec
  s.dependency "ExamplePod"

これを pod lib lintで検証すると、ExamplePodがみつからず以下の様なエラーになります。

$ pod lib lint

[!] Unable to find a specification for `ExamplePod` depended upon by `ExamplePod2`

ExamplePodはprivate podspecなので、そのままでは見つからないのです。しかしPodfileでは sourceでprivate Specsを指定できましたが、 pod lib lintによるpodspecの検証のときはPodfileは使われないので、private podspecを参照できないのです。そして pod repo addで指定したSpecsが暗黙のうちに使われるということもありません。

このため、pod lib lint--sourcesオプションでprivate Specsを指定する必要があります。デフォルトのCocoaPods/Specsも必要なので、カンマ区切りでURLのリストを渡します。これが三つ目のポイントです。

$ pod lib lint --only-errors --sources='git@github.com:gfx/PrivateSpecsExample.git,https://github.com/CocoaPods/Specs'

 -> ExamplePod2 (0.0.2)
    - WARN  | [source] Git SSH URLs will NOT work for people behind firewalls configured to only allow HTTP, therefore HTTPS is preferred.
    - WARN  | [iOS] Unable to find a license file

ExamplePod2 passed validation.

これで通常通り検証ができました。 pod repo pushは検証のロジックがlintとは違うらしく、 --sourcesは不要です。

$ git tag 0.0.1 && git push origin 0.0.1
$ pod repo push --allow-warnings  myspecs *.podspec

これですべて問題なくできました。各種コマンドのオプションがが複雑なので、Makefileを作っておきましょう。CIでログなどをとるため、 --verboseもつけておくといいでしょう。

lint:
    pod lib lint --verbose --only-errors --sources='git@github.com:gfx/PrivateSpecsExample.git,https://github.com/CocoaPods/Specs'

release:
    pod repo push --verbose --allow-warnings  myspecs *.podspec

.PHONY: lint release

これでprivate Specsを使う準備はすべて整いました。

まとめ

CocoaPod private Specsはいくつか運用のポイントがあり、本エントリではそれらを紹介しました。private Specs周りはハマるところが多く、バージョンによって挙動が異なることも多いので、安定して運用できるよう情報を共有できればと思います。

Swiftで遊んでますか?

モバイルファースト室の三浦です。

みなさんはplayground使っていますか?

Swiftにはplaygroundが用意されていて手軽にかつライブレンダリングでコーディングをすることができます。 CoreGraphicsの描画などを確認しながらコードを書くこともできてとても便利です。

早速Swiftで簡単なスケッチをしてみましょう!

Xcodeでplaygoundファイルを新規作成します。次にUIKitをimportします。

import UIKit

次に表示のためのUIViewを生成します。

// ビューのサイズ
let size = CGSize(width: 200, height: 200)
// UIViewを生成
let view:UIView = UIView(frame: CGRect(origin: CGPointZero, size: size))
view.backgroundColor = UIColor(white: 0.9, alpha: 1.0)
// PlaygroundのTimelineに表示するためのview
let preview = view

Image may be NSFW.
Clik here to view.
20141112192138
previewの行の右端をマウスオーバーして表示される+ボタンをクリックすると、タイムラインにその行の実行結果が表示されます。 previewは常にコードの最終行にします。

本来はplayground用に用意されているXCPlaygroundフレームワークのXCPShowViewを使ってTimelineに表示することが可能ですが、現行のXcode6.1でiOS用にUIKitを使って表示した場合コンソールにエラーが出てしまうため使用していません。

早速線を描画してみます。

// CoreGraphicsで描画する
UIGraphicsBeginImageContextWithOptions(size, false, 0)

// 描画する
let path = UIBezierPath()
path.moveToPoint(CGPointMake(50, 100));
path.addLineToPoint(CGPointMake(150, 100))
UIColor.orangeColor().setStroke()
path.stroke()

// viewのlayerに描画したものをセットする
view.layer.contents = UIGraphicsGetImageFromCurrentImageContext().CGImage

UIGraphicsEndImageContext()

Image may be NSFW.
Clik here to view.
20141112192139

右の目のアイコンを押せば行単位の実行結果も確認できます。 strokeのカラーなどを変えれば即座に色が変わります。 Image may be NSFW.
Clik here to view.
20141112192140

UIパーツをつくってみる

Image may be NSFW.
Clik here to view.
過去の記事 iOSアプリデザインリニューアルの舞台裏で記載していましたが、クックパッドアプリの中でもUIパーツの一部はコードで実装されています。 コード化することでわざわざ画像を用意しなくて済み、さまざまサイズにも柔軟に対応することができます。

左上と右下が角丸のおすすめバッヂも下記のように生成できます。

Image may be NSFW.
Clik here to view.
20141112192141

描画部分のコードは以下のようになっています。

// CoreGraphicsで描画する
UIGraphicsBeginImageContextWithOptions(size, false, 0)

// アイコン画像を描画する
let image = UIImage(named: "image")
image?.drawInRect(CGRectMake(0,0, 192, 192))

// バッヂの背景を描画する
let rect = CGRectMake(0, 0, 96, 36)
let roundCorner = UIRectCorner.TopLeft | UIRectCorner.BottomRight
let roundSize = CGSizeMake(6.0, 6.0)
let path = UIBezierPath(roundedRect: rect, byRoundingCorners: roundCorner, cornerRadii: roundSize)
UIColor(red: 0.545, green: 0.678, blue: 0.0, alpha: 1.0).setFill()
path.fill()

// 文字を描画する
let attrString = NSAttributedString(
    string: "おすすめ",
    attributes:[NSForegroundColorAttributeName: UIColor.whiteColor(),
        NSFontAttributeName: UIFont.boldSystemFontOfSize(20.0)])
attrString.drawAtPoint(CGPointMake(6, 4))

新規のパーツはデザインイメージと違いがないように文字位置やサイズなどコード上で微調整をする必要がありますが、 playgroundであれば描画結果を見ながら調整することができるのでとても便利です。 プロジェクトに組み込む前にも手軽に確認しておくことができます。

まとめ

今回は静的なパーツをつくるところまでなので開発速度における大きなメリットはでにくいですが、 少し複雑なアイコンをパスで描く際や、パスをさらにアニメーションさせるときなどは、変化させたいプロパティを調整していけばコンパイルすることなく動きを確認できるので、playgroundであらかじめ試作しておけばプロジェクトの組み込み時には確度の高いアニメーションを実現することができます。 そしてなによりコードを書いていて楽しい!

アプリ開発にもぜひplaygorundを活用してみてください!


iOSアプリ間連携の実装に x-callback-url を使う

はじめに

モバイルファースト室の @slightairです。 クックパッドが提供しているiOSアプリには、連携して機能するものがあります。

買い物リストアプリを例に挙げると、クックパッドアプリのレシピ画面からレシピに使われている材料を買い物リストアプリに登録することができます。

Image may be NSFW.
Clik here to view.
f:id:Slightair:20141113101333p:plain

この機能は、x-callback-urlという仕様に沿って実装しています。 x-callback-url は別のアプリの呼び出しや情報の受け渡しに使うカスタムURLスキームの形式を定義するものです。 この仕様に沿って実装することで、他のアプリから呼び出せる処理や必要なパラメータをきれいにまとめることができます。

この記事では x-callback-url を用いたアプリ間連携の実装について説明します。

カスタムURLスキーム

iOSアプリで他のアプリに遷移しつつなにかしらの情報を渡すにはカスタムURLスキームを使う事になると思います。 遷移先のアプリでは叩かれたURLを受け取れるので、URLをパースして渡ってきた値を解釈したり、次にどのような処理をするか決めることができます。

どのような形式のURLを受け付けるかはアプリそれぞれで定義することになります。 URLで表現できるものであればなんでもよいのですが、ここで好き勝手に形式を決めてしまうと、よほどうまくやらない限りいつか破綻してしまうでしょう。

x-callback-url

クックパッドでは、アプリ間連携を x-callback-urlという仕様に沿って実装しています。 この仕様では、パラメータをURLに含める形式を定めています。 x-callback-url では、他のアプリに渡す値だけではなく、他のアプリに委譲した処理の結果を受け取るためのパラメータも決められています。

具体的には以下の様な形式です。

[scheme]://[host]/[action]?[x-callback parameters]&[action parameters]

actionに遷移先のアプリに委譲したい処理名を、action parametersには action に必要なパラメータを指定します。 x-callback parametersには、遷移先のアプリで表示するために使う遷移元のアプリ名(x-source)、成功時に遷移元のアプリに戻るためのURL(x-success) などを指定します。 詳しくは x-callback-url の仕様を読んでみてください。

x-callback-url は Google Chrome でも使われているようです。 https://developer.chrome.com/multidevice/ios/links#using-the-x-callback-url-registration-scheme

このページに書かれている仕様に沿ってアプリを実装すると、Webページを開く際にChromeを使い、ユーザがページの閲覧を終えたら元のアプリに戻ってくるような動きをさせることができます。

実装

x-callback-url の仕様に沿ったアプリ間連携を簡単に実装するためのライブラリ InterAppCommunicationがあります。 クックパッドではこのライブラリをラップし、他のクックパッドのアプリの機能を簡単に呼び出せるようにして社内共通ライブラリに組み込んでいます。

呼び出し側の実装例

InterAppCommunication を使うと、他のアプリの機能を呼び出すときには以下のように書けます。

    IACClient *client = [IACClient clientWithURLScheme:@"url-scheme"];

    [client performAction:@"action"
               parameters:@{
                             ...
                            }
                onSuccess:^(NSDictionary *params) {
                    ...
                }
                onFailure:^(NSError *error){
                    ...
                }
     ];

受け側の実装例

application:didFinishLaunchingWithOptions:でcallbackURLSchemeとアクションに対応する処理を書くデリゲートをセットします。

    [IACManager sharedManager].callbackURLScheme = @"url-scheme";
    [IACManager sharedManager].delegate = <IACDelegateに準拠したクラスのインスタンス>;

application:openURL:sourceApplication:annotation:で IACManager のインスタンスメソッド handleOpenURL: を使い、呼び出された url をわたします。

if ([[url scheme] isEqualToString:@"url-scheme"]) {
        return [[IACManager sharedManager] handleOpenURL:url];
    }

上記2つのコードは、呼び出し側で処理が終わった後に元のアプリに戻ってくる場合にも必要です。

IACManager の delegate に設定するインスタンスのクラスで supportsIACAction:performIACAction:parameters:onSuccess:onFailure:を定義します。

- (BOOL)supportsIACAction:(NSString *)action
{
    NSArray *supportedActions = @[@"action1", @"action2", ...];
    return [supportedActions containsObject:action];
}

- (void)performIACAction:(NSString *)action
              parameters:(NSDictionary *)parameters
               onSuccess:(IACSuccessBlock)success
               onFailure:(IACFailureBlock)failure
{
    if ([action isEqualToString:@"action1"]){
        // action1 の処理// 結果に応じて success/failure block を呼ぶ
    }
    ...
}

ライブラリが x-callback-url の仕様に沿ったURLを解析して、パラメータやsuccess/failureブロックに展開してくれます。

おわりに

クックパッドで公開しているアプリのアプリ間連携の実装の話をしました。 別のアプリに遷移して、再びアプリに戻るような動きがなかったとしても、x-callback-url のような仕様に合わせたほうがきれいに実装できます。 自前でURLクエリをパースする処理を書いたり、独自の形式を考えるより楽だと思います。

スムーズなアプリ間連携を実装して、ユーザーに気持ちのよい体験を届けられるとうれしいですね。

Infratasterでリバースプロキシのテストをする

インフラ部の荒井(@ryot_a_rai)です。この記事ではインフラの振る舞いテストのツールであるInfratasterを使ってリバースプロキシの設定のテストをしてみたいと思います。

Infratasterとは


Infratasterはインフラの振る舞いをテストするフレームワークで、RSpecのテストヘルパとして機能します。例えば、

  • 特定のヘッダ付きのHTTPリクエストを送信した時にあるレスポンスヘッダが返ってくることをテストする
  • Capybaraを使って実際のWebブラウザ上での挙動をテストする
  • MySQLのSHOW VARIABLESの結果をテストする

といったことが可能になります。

細かい概要についてはこちらのスライドREADMEをご覧ください。

Serverspecとの違い

インフラのテストといえばServerspecが有名かと思いますが、InfratasterはServerspecとはテストする領域が異なっています。Serverspecはサーバの内部のミドルウェアやファイルをテストしますが、Infratasterはサーバの外部からテストをします。つまり、Infratasterは中で動いているミドルウェアが何かは関係なく、外から見てどういった振る舞いをするかを検証するためのものです。

Infratasterを使ってnginxの設定のテストをする

上で紹介したInfratasterと仮想マシン(Vagrant, VirtualBox)を使ってnginxの設定が意図したとおり行われているかをテストしてみたいと思います。

本記事のコードは https://github.com/ryotarai/proxy-configtest-sampleにあります。

仮想マシンを用意する

Vagrantをつかってテスト用の仮想マシンを用意します。Vagrantで仮想マシンを起動、セットアップすることで再現性のあるテスト環境を用意することが可能になります。

VirtualBox、Vagrantをインストール後、プロキシ用、バックエンド用に1つずつVagrant VMを立てますが、今回はこちらのVagrantfileを使います。このVagrantfileでは、proxy VMに/etc/hostsを置いて、バックエンドへのアクセスをapp VMに向けてテストしやすくしています。

Vagrantfileを置いたあと、vagrant upを実行しておきます。

Image may be NSFW.
Clik here to view.
f:id:ryotarai:20141118112005p:plain

Infratasterをインストールする

Infrataster, RSpecをGemとしてインストールします。

# Gemfile
source 'https://rubygems.org'
gem 'infrataster'
gem 'rspec-json_matcher'

rspecコマンドを使って、テストに必要なファイルのひな形を生成します。

$ bundle exec rspec --init

Infrataster(とrspec-json_matcher)を使うために、生成されたspec/spec_helper.rbの先頭に以下を追記します。

require'rspec/json_matcher'require'infrataster/rspec'RSpec.configuration.include RSpec::JsonMatcherInfrataster::Server.define(
  :proxy,           # name'192.168.0.0/16', # proxy VM's IP addressvagrant: true# for vagrant VM
)

200が返ってくることをテストする

準備ができたので、プロキシに対するテストを書いてみます。手始めに、http://foo.example.comにアクセスした時に200が返ってくることをテストします。

# spec/foo_spec.rbrequire'spec_helper'

describe server(:proxy) do
  describe http('http://foo.example.com') do
    it 'returns 200'do
      expect(response.status).to eq(200)
    endendend

まだnginxの設定を書いていないので、テストは失敗します。

$ bundle exec rspec
  1) server 'proxy' http 'http://foo.example.com' with {:params=>{}, :method=>:get, :headers=>{}} returns 200
     Failure/Error: expect(response.status).to eq(200)
     Faraday::ConnectionFailed:
       Connection refused - connect(2) for "192.168.33.10" port 80

テスト対象のnginxの設定を書きます。

# nginx/foo.conf
server {
  listen 80;
  server_name foo.example.com;
  location / {
    proxy_pass http://app-001/;
  }
}

このままだとproxy VM内でapp-001が名前解決できないので、/etc/hostsでapp VMに向けます。

# Vagrantfile
-hosts = %w!!
+hosts = %w!app-001!

再度テストを走らせると、通ることが確認できると思います。

$ vagrant provision
$ bundle exec rspec
server 'proxy'
  http 'http://foo.example.com' with {:params=>{}, :method=>:get, :headers=>{}}
    returns 200

Finished in 0.01166 seconds
1 example, 0 failures

意図したホストにプロキシされているかをテストする

つぎに、意図したホストにプロキシされているかをテストしてみます。app VMのnginxでX-MOCK-HOSTヘッダをつけるようにしたので、これを使います。モック用のアプリはリクエストヘッダをそのままJSONにして返すようになっているので、レスポンスをテストすることでプロキシ→バックエンドのリクエストヘッダをテストすることができます。

--- a/spec/foo_spec.rb+++ b/spec/foo_spec.rb@@ -5,6 +5,12 @@ describe server(:proxy) do
     it 'returns 200' do
       expect(response.status).to eq(200)
     end
++    it 'proxies to app-001' do+      expect(response.body).to be_json_including({+        'X_MOCK_HOST' => 'app-001',+      })+    end
   end
 end
$ bundle exec rspec
server 'proxy'
  http 'http://foo.example.com' with {:params=>{}, :method=>:get, :headers=>{}}
    returns 200
    proxies to app-001

Finished in 0.01675 seconds
2 examples, 0 failures

無事、プロキシ先のホストの確認ができました。

レスポンスヘッダをテストする

プロキシでCache-Controlヘッダを返すようにしてみます。先にテストを書いて、失敗させてから設定を書きます。

--- a/spec/foo_spec.rb+++ b/spec/foo_spec.rb@@ -12,5 +12,11 @@ describe server(:proxy) do
       })
     end
   end
++  describe http('http://foo.example.com/isucon') do+    it 'returns Cache-Control header' do+      expect(response.headers['Cache-Control']).to eq('max-age=86400')+    end+  end
 end
$ bundle exec rspec
  1) server 'proxy' http 'http://foo.example.com/isucon' with {:params=>{}, :method=>:get, :headers=>{}} returns Cache-Control header
     Failure/Error: expect(response.headers['Cache-Control']).to eq('max-age=86400')

       expected: "max-age=86400"
            got: nil

       (compared using ==)
     # ./spec/foo_spec.rb:18:in `block (3 levels) in <top (required)>'

nginxの設定を追加します。

--- a/nginx/foo.conf+++ b/nginx/foo.conf@@ -1,6 +1,12 @@
 server {
   listen 80;
   server_name foo.example.com;
++  location /isucon {+    expires 24h;+    proxy_pass http://app-001/;+  }+
   location / {
     proxy_pass http://app-001/;
   }

再度テストを実行すると、適切に設定されていることが確認できます。

$ vagrant provision
$ bundle exec rspec
server 'proxy'
  http 'http://foo.example.com' with {:params=>{}, :method=>:get, :headers=>{}}
    returns 200
    proxies to app-001
  http 'http://foo.example.com/isucon' with {:params=>{}, :method=>:get, :headers=>{}}
    returns Cache-Control header

まとめ

Infratasterを使ってリバースプロキシの設定をテストする方法を紹介しました。本記事ではnginxを例に出しましたが、Apacheやその他のソフトウェアでも同様にテストすることができます。

プロキシの設定は徐々に複雑化していき、挙動が見えなくなっていきがちです。テストを書いておけば、意図しない挙動の変更を防げたり、テスト自体をドキュメントとして使うこともできます。この記事がInfratasterを使ったテストの参考になれば幸いです。

モバイルアプリのログ収集ライブラリ「Puree」をリリースしました

モバイルファースト室の @rejasupotaroです。

クックパッドでは、サービスをリリースしてログを収集して分析して改善してまたリリースして、というサイクルを素早く回すことでより良いものを作るということをウェブではやってきました。 クックパッドのサービス開発のフレームワークをモバイルアプリでも適用したいのですが、モバイルアプリにはウェブアプリと違ったロギングの難しさがあります。

今回はモバイルアプリのロギングの問題点とPureeというログ収集ライブラリについて話します。

モバイルアプリのロギングの難しさ

ウェブアプリでは、基本的にはサーバー側でログを収集することができますが、モバイルアプリの場合は画面の制御はアプリ側で行われ、APIを介してデータを受け取るため、クライアント側でログを収集して送信する必要があります。

Image may be NSFW.
Clik here to view.
f:id:rejasupotaro:20141123225718p:plain

アプリのログを収集するのに、画面遷移をしたりタップするたびにサーバーにログを送るということも考えられますが、パフォーマンスやデータ使用量やバッテリーのことを考えるとあまり現実的ではありません。

Image may be NSFW.
Clik here to view.
f:id:rejasupotaro:20141123224948p:plain

また、モバイルアプリは動的に接続状況が変わるので送信に失敗したログをケアしなければなりませんし、そのためにはログをバッファリングする必要があります。

Image may be NSFW.
Clik here to view.
f:id:rejasupotaro:20141123225027p:plain

そこでクックパッドではPureeというライブラリを作って、モバイルアプリからはPureeを介してログを送信しています。

ログ収集ライブラリPureeについて

Pureeは以下の機能を備えています。

  • フィルタリング: 共通のパラメータを付与したり、サンプリングを行ったりすることができます。
  • バッファリング: ログを一時的に貯めて、送信に失敗したときや端末の再起動などでログが消えないようにする役割を持っています。
  • バッチ: 複数のログを一つにまとめて送ることができます。
  • リトライ: ログの送信に失敗した場合は、一定時間経過後に自動で再送信を試みます。

Image may be NSFW.
Clik here to view.
f:id:rejasupotaro:20141123224958p:plain

また、Pureeはアウトプットプラグインという形でログの送信先の切り替えたり、フィルターを適用したりすることができます。

Image may be NSFW.
Clik here to view.
f:id:rejasupotaro:20141123232523p:plain

これらの実装は Fluentdを参考にしています。

Pureeの使い方

ログを送る

例として、タップのログを定義します。 ログはJsonConvertibleクラスを継承します。

publicclass ClickLog extends JsonConvertible {
    @SerializedName("page") private String page;
    @SerializedName("label") private String label;

    public ClickLog(String page, String label) {
        this.page = page;
        this.label = label;
    }
}

ログを定義したら任意の箇所で Puree.sendメソッドにログと送り先を渡します。

Puree.send(new ClickLog("MainActivity", "track"), OutLogcat.TYPE);

アウトプットプラグインを定義する

アウトプットプラグインにはPureeOutputとPureeBufferedOutputの二種類があります。 PureeOutputはバッファリングは行わず、ただちにログの出力を行います。Google AnalyticsやMixpanelのようなライブラリ内にバッファのしくみがあるようなものに出力する場合はこちらを使います。

例えば、ログをLogcatに出力するプラグインは下のようになります。

publicclass OutLogcat extends PureeOutput {
    publicstaticfinal String TYPE = "logcat";

    @Overridepublic String type() {
        return TYPE;
    }

    @Overridepublic OutputConfiguration configure(OutputConfiguration conf) {
        return conf;
    }

    @Overridepublicvoid emit(JSONObject jsonLog) {
        Log.d(TYPE, jsonLog.toString());
    }
}

PureeBufferedOutputを継承すると、バッファリングとリトライを自動で行うようになります。 デフォルトのインターバルは2分になっていて、現状ではログの送信に失敗すると baseInterval * (retryCount + 1)後に再送信を試みます。また、デフォルトの最大リトライ回数は5回に設定されていて、5回連続で失敗した場合は次回のイベント発火時に送られます。 一度に送るログの量も制限することができます。

publicclass OutBufferedLogcat extends PureeBufferedOutput {
    publicstaticfinal String TYPE = "buffered_logcat";

    @Overridepublic String type() {
        return TYPE;
    }

    @Overridepublic OutputConfiguration configure(OutputConfiguration conf) {
        return conf;
    }

    @Overridepublicvoid emit(JSONArray jsonLogs, AsyncResult asyncResult) {
        Log.d(TYPE, jsonLogs.toString());
        asyncResult.success();
    }
}

emitメソッドに出力するログが入ってくるので、そこでGoogle AnalyticsやMixpanelに送ったり、APIを叩いたりします。 実際にPureeBufferedOutputを使う場合には非同期で結果を受け取ることになると思うので、ログの送信に成功したか失敗したかを知らせるためにコールバックで asyncResult#successasyncResult#failを呼ぶ必要があります。

@Overridepublicvoid emit(JSONArray jsonLogs, final AsyncResult asyncResult) {
    client.sendLogs(jsonLogs, new Callback() {
        @Overridepublicvoid success() {
            asyncResult.success();
        }

        @Overridepublicvoid fail() {
            asyncResult.fail();
        }
    });
}

フィルターの定義

Pureeのsendが呼ばれたあとに、対応したフィルターのapplyメソッドが呼ばれます。 そこでログに共通するパラメータを付けたり、特定の条件のときには送らないようにするなどができます。 たとえばイベントの起こった時間を付与するフィルターは下のようになります。

publicclass AddEventTimeFilter implements PureeFilter {
    public JSONObject apply(JSONObject jsonLog) throws JSONException {
        jsonLog.put("event_time", DateUtils.getTimestamp());
        return jsonLog;
    }
}

Pureeの初期化

Pureeでログを送るには前もってプラグインを登録する必要があります。 初期化はApplicationクラスの中などで行います。

publicclass MyApplication extends Application {

    @Overridepublicvoid onCreate() {
        Puree.initialize(buildConf(context));
    }

    publicstatic PureeConfiguration buildConf(Context context) {
        PureeFilter addEventTimeFilter = new AddEventTimeFilter();
        returnnew PureeConfiguration.Builder(context)
                .registerOutput(new OutLogcat(), addEventTimeFilter)
                .registerOutput(new OutBufferedLogcat(), addEventTimeFilter)
                .registerOutput(new OutDisplay(), new SamplingFilter(0.5F))
                .registerOutput(new OutBufferedDisplay())
                .build();
    }

第一引数にアウトプットプラグインを渡して、第二引数以降にオプションでフィルターを渡します。

Pureeの導入方法

Pureeはjcenterで公開していますので、build.gradleのrepositoriesにjcenterを追加した上で、dependenciesにpureeを追加してください。

// build.gradle
buildscript {
    repositories {
        jcenter()
    }
    ...

// app/build.gradle
compile 'com.cookpad:puree:1.0.0'

ソースコードは GitHubで公開しています。

まとめ

Pureeを作る前はログの出力先が増えたり、プロジェクトが変わったりするたびに独自にログを収集するしくみを作っていました。 ログはサービスを成長させるためには重要です。Pureeを導入することでログを収集するしくみを実装する時間を短縮して、その分「どのログを取るか」「どうやってログを活かすか」を考える時間に使えればいいなと思います。

Pureeは今のところはAndroid版だけを公開していますが、iOS版の開発も進んでいるので、近いうちに公開されると思います。

サービス開発エンジニアからマネージャになった話

はじめに

こんにちは、レシピ投稿推進室の勝間(@ryo_katsuma)です。 techlifeでの執筆は5年ぶり(!)になります。

さて、そんな私も今年2014年の5月にエンジニアからサービス開発の部署のマネージャに転身しました。 そこで今回のtechlifeブログは、いつもの技術ネタとは少し異なるテーマとして、「クックパッドにおいて、エンジニアからマネージャに転身する」ことが、どういうことなのかを自分自身で振り返り、まとめたいと思います。 エンジニアが自身のキャリアを考える上で、少しでも参考になれば幸いです。

現状

私は、2009年5月に中途入社し、今年で6年目になります。この年数は、エンジニア全体はもちろん、社内全体で見ても古い方になります。 これまで、技術部、HappyAuthor部(現在、私が所属している前身になった部)、新規事業部、プレミアム会員事業部...など、いろんな部署でサービス開発のエンジニアを行ってきました。

今は、クックパッドにレシピをのせてくれるユーザーさん向けのサービス開発を行う「レシピ投稿推進室」という部署のマネージャを行っています。 日々、ユーザーさんが「クックパッドにレシピを投稿してよかった」と思ってもらえるようなサービスや仕組みをメンバーと考えています。 メンバーの数で言うと、自分を除いて10名のメンバーが所属しています。この数は、会社全体で見ても1つの部署としては、メンバー数は多くもなく少なくもなく、平均的な数字になっています。

マネージャへの転身

背景

まず、マネージャというポジションについては、2, 3年ほど前から興味は持っていました。 ただ、明確に「いつまでにこの部署のマネージャとして」という強い思いではなく、 「機会があればいつか挑戦してみたい。」くらいのふわっとした思いでした。 当時はまだ自分事として捉えておらず、まずはエンジニアとしての成果を出すことに集中していました。

一方で、エンジニアとして技術一本で生きていけるかと言われると、それも怪しいと自分自身で考えていました。 特に、2010年辺りからは、業界内でも有名なプロダクトを作ってきたことがあるようなエンジニアもどんどん入社してきて、彼らと技術力だけで競い合っていくのは、難しいと感じていました。 つまり、エンジニアとして技術ではなくサービスを生み出す力を伸ばしていくか、ポジションを変えてマネジメントの力を付けていくか、どちらかに絞る必要が出てくるだろうなと、ここ数年は感じてました。

そんな中、ここ2年ほどは「エンジニアリーダー」としての仕事をいくつかの部署で経験することができていました。 エンジニアに対する、ガッチリとしたマネジメントまで行わないものの、部署のエンジニアのコードレビューを行ったり、部のエンジニア評価を行ったり、いわゆるエンジニアリングに関するマネジメントを統括的に見ていました。

これは、マネージャとエンジニアの間を取り持つポジションにもなるのですが、普段の業務の中でのエンジニア間の雰囲気作りや、部のエンジニアリングの方向性決めなどを行うことも、当時の部署のマネージャに少しつづ評価されはじめました。 実際、自分自身も少しづつこの仕事にやり甲斐を感じ始め、「少なくとも全く向いていないことは無さそうだな」と感じ始めてました。

突然の依頼

そのままマネージャというポジションへの考えをまだ明確に持っていない日が続いていた中、 ある日、現在の部署の前部長に「ウチの部のマネージャになってみない?」という打診を突然受けました。

依頼理由としては、

  • ちょうど年度の変わり目で組織改編が起こり、前部長が新しい部署に移ることが決まっていた
  • 前部長ともエンジニアリーダー関連でよく一緒に仕事で絡んでいて、人なりがお互い分かっていた

などがありますが、「自分が務まるものなのかな…」と、驚いたことは事実でした。

ただ、

  • 依頼がかかった部署が自分が昔所属していた部で、思い入れも強い部署だった
  • 会社の事業を支える「レシピ」を投稿してくれるユーザーさんを向いた部署、ということで特にやり甲斐を感じる部署だった
  • マネージャに限らず、個人の可能性を広げる挑戦は会社が歓迎してくれる姿勢を見せていた

ということもあり、悩んだ結果、このタイミングで依頼を受けることにしました。

選択肢として、当時の部署に残ってサービス開発に専念するという手段も取れましたが、

  • 極端な話としてマネージャになった後でサービス開発エンジニアに戻って再挑戦することは可能(実例
  • マネージャは後からなりたいと言っても状況によってなれない可能性も高い

ということを考えて、挑戦するならこのタイミング、と判断しました。

インプット

前述の通り、エンジニアリーダーの経験はあるものの、まともにマネージメント行うことは初めてです。 まずは、できるだけインプットを増やした方がいいかと考え、先人の考えを理解するためにもマネージメント系の本をいくつか読みました。

いくつか例に挙げると

など。 まずは、有名な名著を中心に、さらっとエッセンスを学べるものを数多く読んでいました。 ただ、これらの本も何も自分が動いていない状態で読むのと、動いたことがある状態で読むので 受け取り方が大きく変わるので、今あらためて読みなおしているところです。

また、実際に期の変わる前にメンバーとの面談を行いました。その中で

  • 今後どんなタイプのエンジニア/ディレクタになっていきたいか
  • どんな挑戦をしてみたいと考えているか

などの質問を行いました。 部の目標に向けた挑戦を行うことはもちろんですが、それ以外についてメンバーそれぞれがどういうところを目指して挑戦すべきか、それについて私がどういう手助けができるかを理解しておこうと考えました。

業務

実際にマネージャ業務を開始してからは、次のようなことを行いました。

  • 部全体の目標とKPI設定
  • 目標に向けた施策の遂行
  • 部内での定期的な進捗確認
  • 他の部のマネージャや、担当執行役との定期的な共有
  • エンジニア採用のために人事部への協力
  • 全社横断的な課題について、解決策の提案
  • ...etc

こう見ると、実際の部の業務以外のことの項目が多いことが分かります。 クックパッドもそれなりの規模の会社なので、他部署との連携を中心に組織的な課題への取り組みに時間を割くことは マネージャとしてある意味仕方がないのかもしれません。

自分なりに気をかけたこと

マネジメントという業務に対してはまだまだ経験もテクニックもないので、部内でできることも少ないのが事実です。 そんな中で、自分の中で強く信じられることは、特に注意して行動することを心がけていました。

やったことがないことに挑戦する機会を作る

よほど意識をしない限り、自分たちは慣れた手段、慣れたことをずっと行いがちです。 ただ、今までと同じようなことだけをやっても、個人も組織も新しい挑戦を行わないので成長に繋がることはありません。

なので、「個人として今までやったことがないことをやってみて、その結果として部の目標を達成できることは何なのか」を 考えるようにしていました。 たとえば、ここでは期初の面談で話していた「実は挑戦してみたい」と考えているものをできるだけ実際に挑戦してもらい、結果として目標数字に繋がる形を目指してもらうことにしました。(例:個人開発でしか触ったことがなかったAndroidアプリ開発を業務で行う)

新しい分野の挑戦は、本人の能力の幅を広げる機会になり、結果として組織内外においてその人の市場価値を高めることになるので、積極的に勧めていました。

結果、他の部署から「最近xxさん、いい感じだよね」のような声が上がってくることもありましたが、 マネージャとしては、社内からメンバーが褒められるような声を聞くのが嬉しいものです。

情報を積極的に伝える

マネージャ間で共有されるような社内の情報は、できるだけ早くメンバーに共有するようにしています。 自分自身の経験でもありますが、上から情報がなかなか降りてこないことがあると、 会社で何が起きているか不明瞭になり、組織に不信感を覚えることにつながり得ることになると考えています。

なので、

  • 自分が得ている情報は人事やインサイダーに関わる情報以外は極力全部口頭で伝える
  • 社員だけではなく、パートスタッフにも伝える

ということを意識してきました。

実際、共有する情報は全てが全て、実際に必要となる有益な情報ではないことが多いです。 ただ、そこは情報を受け取る側が取捨選択してもらえばよくて、 情報を咀嚼する中で会社がやってることに興味感心を持ってくれることが出てきて、 業務にも活きるものも出てくるかなと考えています。

これについては、定量的にも定性的にもいい評価はできていませんが、「もうやめてください」という声が部内から出てくるまで今後も続けようと思っています。

自分で手を動かすべきか、動かさないべきか

エンジニアからマネージャになった人ならではの話ですが、「自分で手を動かした方がいいのか、そうでないのか」という話について。

エンジニアは、良くも悪くも自分でいざとなったら全て1人で解決できてしまうものです。 ましてやマネージャは意思決定者でもあるので、自分でやることを決めて形にするまでノンストップで実現できてしまいます。

一方、いろんな本や文献を見ても、基本的に「マネージャはマネージメントに徹するべき」と言及しているものが多いです。 社内の前職でマネージメント経験者のエンジニアと話しても「勝間さん、自分でコード書くのはやめたほうがいいですよ」と言われることがありました。

そこまで言うなら、もう自分でコードを書かない方がいいのかな。。と思いつつも半信半疑。「自分はプレイングマネージャを極めたい!」と意固地になるつもりも別にないですが、実際に試して納得しないと気が済まない性格なので、自分自身でどういうメリット・デメリットがあるのか理解するまで試すことにしました。

サービス開発も自分で拾う

最初は、サービス開発も自分で拾うようにしていました。

  • アプリ開発は他のエンジニアが開発
  • Webは自分が開発

という領域で分けていましたが、 結局目の前の開発を行うことだけに集中しすぎて、部で進行させている施策全体を俯瞰して見れなくなってきました。

また、他の業務に忙殺されて、開発業務を気合だけで進めるようにするものの、結局時間がかかってしまい、改善も後手後手で勧めづらい状態になり、やり方を変更せざるをえないことになりました。(そういえば、最終的にはObjective-C勉強してアプリの開発にも手を出していたこともありました!)

溢れているタスクだけ拾う

メインの開発を拾っても、なかなかうまく回らないと自分で思ったので、 部内で、優先度の観点で溢れているIssueの対応を行うようにしました。 つまり、メインのサービス開発以外の改善系のタスクだけを拾うようにしました。

結果としては、

  • メインの開発を拾っていたときと比べると、以前より全体を俯瞰することはできるようになった
  • 一方で、現状を改善させることにやはり集中しすぎて、メンバーとのコミュニケーションがおろそかになった

ということがありました。

「やはり開発は控えて自分がやるべきことに時間をかけるべき。」と判断し、 人事に協力を依頼して、学生アルバイトのエンジニアを採用し、細かな改善系のタスクも依頼するようにしました。 また、メンバーともできるかぎり定期面談を行い1-1でじっくりコミュニケーションを取れるように工夫しました。

こうして振り返ってみても、「自分もできるから」ということを理由に開発に手を伸ばすことは、 避けたほうがやはり良さそうです。(先人の考えは正しかったですね。。) これ以降は自分に余力があるときのみ、それもメインの開発以外のものだけに絞って手を伸ばすように意識を変えました。 プレイングマネージャを目指すよりも、部のメンバーたちで成果を上げることを目指すほうがいいチームになりそうです。

マネージャになって良かったこと、辛かったこと

マネージャになることで、実際のところ何が良くて何が悪いのでしょうか。 細かいことを言うといろいろありますが、大きいと考えていることをそれぞれ1つづつ挙げようと思います。

自分の責任で実現したい世界を実現できる

「自分はこういう世界を実現したい」と描くものは、目的とその内容がメンバーに理解されれば、 あとは細かな進め方をを相談することで、実現することが基本的に可能になります。 仮に、既存のサービスの中で変なしがらみでおかしくなっているようなことも、 自分が責任を持つことで健全なものに変更することができます。

たとえば、クックパッドのアカウント登録と、レシピ投稿に必要なキッチン開設はずっとフローが分かれていましたが、 「そもそも分かれている合理的な理由は無いよね?」ということをメンバーと相談・協力して、全プラットフォームでフローを統合しました。

もちろん、意思決定者の発案で進めることになるので、成果が出ると担当したメンバーの成果、失敗するとマネージャの責任、というように考えるべきだと思っています。言い換えると、自分の責任で、実現したいと考える世界は実現することは可能になります。

意思決定の難しさ

良いことと表裏一体ですが、正しい(とされる)意思決定を行うことはマネージャの業務の中で最も難しいと思います。 意思決定を行う中では、その決定するための判断材料が不十分な場合でも、 その場で判断・即決して意思決定しないといけないケースも多くあります。

正直、自分が不慣れな分野で意思決定を行うこともあるので、その決定内容の結果に対して不安に思うことも多いです。 「本当にこれでいいのだろうか、逆の選択肢の方が本当はうまくいくんじゃないだろうか。」など、 意思決定をした後もいろいろな考えが頭に浮かぶことも。

ただ、ここ最近は「正しい意思決定だったかどうかは、未来にならないと絶対にわからない」 「むしろ、結果に影響を及ぼす原因が数多く絡み合っている中で、一つの意思決定がどの程度結果を左右したのか、分からないケースが大半。」という考えに至るようになったことで、意思決定に対するストレスも最近は少し減りました。

ちなみに、

は、このあたりの考えについてよくまとまった1冊になっているので、おすすめです。

まとめ

エンジニアからマネージャになるまで、なってから感じたこと、考えたこと、実践したことなどについて 述べてきました。いろいろなトピックを出しましたが、ここで伝えたいことは 「エンジニアはみんなマネージャになろう!」と勧める話ではもちろんありません。 「マネージメントを業務では行っていない人たち」に向けた「マネージメントの世界」の片鱗を 伝えるための話です。

確実に言えることは、エンジニアをやるだけでは体験できない世界をマネージャになることで見ることができます。 その中では、自分の中で確実に変化が起きることになりますが、その変化を楽しめる、面白がれそうな人は トライをしてみるのがいいと思います。

社内のエンジニアはもちろん、社外のエンジニアの方にとっても、 この記事が将来のキャリアを考える上で、何らかの判断材料の1つになれば、幸いです。

SwiftとObjective-Cのコードを1つのプロジェクトでつかう

こんにちは。モバイルファースト室の中村です。

仕事でSwiftを使うことはまだないのでSwiftについて色々気になっている今日この頃です。

今回はSwiftObjective-C(以下、Obj-C)を1つのプロジェクト内でつかう方法と、両者の相違点について気になった点を紹介したいと思います。

Swift -> Obj-C

まず、SwiftからObj-Cを使う方法です。

SwiftからObj-Cを使うには、[product module name]-Bridging-Header.hを作成します。

※ [ProductModuleName]は通常ProductNameと同じです。ProductNameにアルファベット以外の文字を使っている場合、その文字は( _ )(アンダースコア)に置換されます。

Xcodeのメニュー"File > New > File > (iOS or OS X) > Source > Header File."からファイル名を[product module name]-Bridging-Header.hと指定しプロジェクトに追加します。

このBridging Header Fileに、SwiftからインポートしたいObj-Cヘッダーファイルを書きます。

// SwiftAndObj_C-Bridging-Header.h

#import "XYZCustomViewController.h"

Image may be NSFW.
Clik here to view.
f:id:nkmrh:20141202093827p:plain

Build Settings > Objective-C Bridging Headerに作成したファイルのパスを指定します。

Image may be NSFW.
Clik here to view.
f:id:nkmrh:20141202093936p:plain

これでBridging header fileに書かれたObj-Cヘッダーファイルが、全てのSwiftファイルから見えるようになりました。

次のようにSwiftからXYZCustomViewControllerのインスタンスが作成できます。

Swift

var controller = XYZCustomViewController()
self.view.addSubview(controller.view)

Obj-C -> Swift

今度は逆に、Obj-CからSwiftを使う方法です。

Obj-CからSwiftをつかうには、次のインポート文を書きます。

Obj-C

#import [ProductModuleName]-Swift.h

[ProductModuleName]-Swift.hファイルはXcodeから自動生成されるもので、開発者が用意する必要はありません。

これでObj-CからSwiftを使うことができます。

Obj-C

#import "SwiftAndObj_C-Swift.h"

// ViewControllerクラスがSwiftで書かれている場合

ViewController* controller = [ViewController new];
controller.view = [self configureView:controller.view];

型の違い

Obj-Cの変数をSwiftで使う場合、両者の型が違うのでコンバージョンやダウンキャストをします。

// CGFloatをFloat型に代入するには、コンバージョンしたい変数を括弧で囲みコンバージョンします。
myFloat = Float(controller.cgfloat)
// NSDictionaryをDictionaryに、NSArrayをArrayに代入するにはas演算子でダウンキャストします。
myDictionary = controller.nsdictionary as Dictionary<String , AnyObject>
myArray = controller.nsarray as Array<AnyObject>

デリゲートパターン

Obj-Cではデリゲートオブジェクトに対してメッセージ送信可能かrespondsToSelector:メソッドで確認後メッセージ送信していました。 Swiftではif-letシンタックスで次のようにスッキリと書けます。

Obj-C

if ([self.delegate respondsToSelector:@selector(delegateMethod:)]) {
  [self.delegate delegateMethod:arg];
}
Swift

if let value = delegate?.delegateMethod?(arg) {
  println(value)
}

?演算子でデリゲートがnilかどうか、メソッドが定義されているかどうかチェックしています。

キー値監視

NSObjectを継承して作成したSwiftクラスは、キー値監視が使えます。監視したいプロパティにdynamic修飾子を追記します。

Swift

class MyObjectToObserve: NSObject {
  // dynamic修飾子を追記します
  dynamic var myDate = NSDate()
  func updateDate() {
    myDate = NSDate()
  }
}

class MyObserver: NSObject {
  private var myContext = 0
  var objectToObserve = MyObjectToObserve()
  override init() {
    super.init()
    objectToObserve.addObserver(self, forKeyPath:"myDate", options: .New, context: &myContext)
  }

  override func observeValueForKeyPath(keyPath: String, ofObject object: AnyObject, change: [NSObject: AnyObject], context: UnsafeMutablePointer<Void>) {
    if context == &myContext {
      println("Date changed")
    }
    else {
      super.observeValueForKeyPath(keyPath, ofObject: object, change: change, context: context)
    }
  }

  deinit {
    objectToObserve.removeObserver(self, forKeyPath: "myDate", context: &myContext)
  }
}

まとめ

Obj-CとSwiftを同時に使う方法と、両者の相違点について書きました。この記事は、Using Swift with Cocoa and Objective-Cから気になった箇所を紹介させていただきました。 iBooks Storeで無料でダウンロードできるので、もしまだという方は1度目を通してみると良いかと思います。

既存のアプリのiPhone 6と6 Plus対応

はじめまして、11月頭にクックパッドに入社したモバイルファースト室のヴァンサン(@vincentisambart)です。

既存のiOSアプリのiPhone 6と6 Plus対応について書きたいと思います。

既存のiOSアプリはiPhone 6対応済みだと明確にOSに示さないと、iPhone 6でも6 PlusでもiPhone 5の画面を拡大したものが表示されます。アプリから見える画面のサイズはiPhone 5と同じ320x568です。

iPhone 6対応がされていると示すには、方法が2つあります:

  • 静的起動画面:iPhone 6とiPhone 6 Plusの画面サイズに合わせた静的な起動画面用の画像をアプリに入れます。
  • 動的起動画面:起動画面をXIBファイルという形でアプリに入れます。

iOS 7以下は動的起動画面を対応していません。iOS 7以下対応のアプリが動的起動画面を使っても問題はありませんが、旧iOS用に静的起動画面も残す必要があります。

起動画面用のXIBファイルは普段のXIBファイルに比べて色々な制限があります。例えばカスタム・クラスを使えません。なので「動的」と言っても主にAuto Layoutを使って要素の位置が動的に決めるだけです。 実際そのXIBファイルを元に静的画像ファイルが自動的に生成され、それが起動時に使用されます。

静的起動画面、動的起動画面、どっちを使った方がいいのでしょうか?Appleの推奨通り、基本的に動的起動画面(XIB)を使った方がおすすめです。特に中央にロゴを表示するだけの起動画面は簡単に作れます。ただし、起動画面が最初の画面のUIを表す場合、そのUIにカスタムな要素が多いと、起動画面XIBでUIの再現が難しい場合があります。その場合だけに静的画像を使った方がいいと思われます。

もっと具体的にその対応をどうすればいいのか見てみましょう。

静的起動画面

iPhone 6とiPhone 6 Plusの解像度に合わせた画像が必要です。このすべてのiPhoneの解像度をリストする記事がおすすめです。 画像のサイズはそのページの「Rendered Pixels」(表示に関わるピクセル数)という項目だけが重要です。「Physical Pixels」(物理的なピクセル数)は画面の本当のピクセル数ですが、OpenGLを使わない限り、アプリは「Rendered Pixels」しか扱わないです。 「Display Zoom」(画面表示の拡大)はiPhone 6の場合、iPhone 5と何も変わらないので気にする必要がありません。iPhone 6 Plusの場合、サイズがiPhone 6と同じですが、縮尺率が3xなので、ピクセル数が違います。ただし、3xの画像がない場合2xのが使われるので、iPhone 6の画面サイズの3xバージョンがなくても問題ありません。 その2〜3つの画像を用意したら、他の画面サイズ用の起動画像と同じところに追加するだけです:

  • Asset Catalogを使う場合、それに入れます。
  • Asset Catalogを使わない場合、Info.plistファイルのUILaunchImagesに入れます。UILaunchImageSizeはもちろん「Rendered Pixels」を入れます。

動的起動画面

起動画面用のXIBは既存のものも使えますし、新しく作ることもできます:

  • 新規作成は、XcodeでFile→New→File…(⌘N)を選んで、iOS→User Interface→Launch Screenで起動画面を作成できます。ファイル名は変えても問題ありません。
    Image may be NSFW.
    Clik here to view.
    f:id:vincentisambart:20141202083302p:plain:w439
    Image may be NSFW.
    Clik here to view.
    f:id:vincentisambart:20141202083307p:plain:w486
  • 既存のXIBを起動画面としても使うため、XIBの編集画面(Interface Builder)で、右のUtilities paneに一番左のタブ(File Inspector)を選びます。そのFile Inspectorに「Use as Launch Screen」をチェックすれば起動画面として使えます。もちろん「Launch Screen」を新しく作成するとそのチェックボックスが初期からチェックされています。
    Image may be NSFW.
    Clik here to view.
    f:id:vincentisambart:20141202083313p:plain:w262

起動画面として使えるXIBを用意したら、Xcodeにどのファイルを使いたいのかを指定する必要があります:ターゲットの設定の「General」タブの「Launch Screen File」項目に起動画面のファイル名を入れます。

Image may be NSFW.
Clik here to view.
f:id:vincentisambart:20141202083311p:plain:w317

アプリをiOS 8以上のSimulator上で起動してみたら、その起動XIBが表示されるはずです。表示されない場合、もう一度Xcodeから起動してみてください。最初の起動で表示されない場合があります。

iPhone 6/6 Plus用の起動画面を用意したことで、アプリが画面のフルサイズを使えるようになりますが、大きい画面でレイアウトが崩れている場合、幅をちゃんと活用していない場合、どうすればいいのか?それは今後紹介しようと思います。

WebPでモバイルアプリの通信量を劇的に削減する

モバイルファースト室の @slightairです。 クックパッドの iOS/Android アプリは、少し前のバージョンからWebP形式の画像をサーバから取得して表示するようにしています。 この記事では、なぜ画像形式をWebPに切り替えたのか、また切り替える上で注意した点などを説明します。

Cookpad アプリと画像

クックパッドのアプリはユーザさんに投稿していただいたレシピを表示するアプリケーションです。その性質上、レシピ画像や調理手順、検索画面のサムネイルなどたくさんの画像をサーバから取得して表示する必要があります。

画像の数が増えたりサイズが大きくなればなるほど通信量が増えます。最近はスマートフォンの画面サイズがどんどん大きくなっているので、それに合わせて取得する画像を大きくしていくとさらにファイルサイズが増え、通信量も増えていってしまいます。

サーバとやりとりするデータが多くなると、画像が表示されるまでユーザは待たされることになってしまいます。 少しでもデータを小さくしたい…。

WebP

WebP とは Google が開発している画像形式のひとつです。 https://developers.google.com/speed/webp/

WebPはウェブサイトの通信量軽減と表示速度の短縮のために開発されました。 JPEGやGIF、PNGを置き換えることができます。 可逆圧縮モード、非可逆圧縮モード、アルファチャンネルなどをサポートしています。 可逆圧縮モードのWebPはPNGと比べて26%小さくなり、非可逆圧縮モードではJPEGと比べて25-34%小さくなるとしています。

クックパッドのアプリで表示しているレシピ画像などは、写真であるためJPEG形式の画像を表示していました。

Image may be NSFW.
Clik here to view.
f:id:Slightair:20141201192839p:plain

この画面を大きく占める、今日のおすすめのレシピの画像をWebP形式に変換してファイルサイズを比べてみましょう。

この画像です。

Image may be NSFW.
Clik here to view.
f:id:Slightair:20141201192916j:plain

レシピはこちらです。

以下の様なコマンドを用いて変換しました。cwebp はGoogleが提供している変換ツールです。

cwebp -q 90 717981.jpg -o 717981_90.webp

以下のような結果になりました。

file namefile size(Bytes)
717981.jpg90,602100.00%
717981_50.webp18,34420.25%
717981_60.webp20,88223.05%
717981_70.webp23,55025.99%
717981_80.webp30,21433.35%
717981_90.webp51,28856.61%

指定するqualityの値にもよりますが、かなりファイルサイズが削減できていますね。 見た目が変わらずこれだけファイルサイズが削減できるのであればとても期待できますね。

WebP形式の欠点には対応アプリケーションが少なく、JPEGやPNG形式と比べて扱いにくいというものがあります。 Facebookではこのような事例があったみたいです。

グーグルの画像形式「WebP」を試行するFacebook--ユーザーからは不満の声も - http://japan.cnet.com/news/commentary/35031278/

しかし、モバイルアプリケーションの場合はPCブラウザと違い表示するだけなので、ユーザさんがブラウザの右クリックメニューからWebPの画像を保存するようなことはできないはずです。 保存したレシピ画像が見られない開けないといったトラブルもなさそうなので、気軽に採用できそうです。

WebP形式の画像をアプリで表示する

WebP形式の画像を表示するにはどうしたらよいのでしょう?

Android の場合は、4.0 以上であれば標準で対応しているとのことです。 http://developer.android.com/guide/appendix/media-formats.html

しかし、クックパッドアプリでは 4.2 や 4.3 の環境で画像の表示が崩れるなどの問題が起きたため、4.4 以上の端末のみを対象にWebPサポートを有効にしています。

iOS の場合は、libwebp を組み込む必要があります。 クックパッドアプリの場合は画像の読み込み・表示に SDWebImageというライブラリを使っていました。このライブラリではオプションでWebPサポートを有効にすることができるので、これを導入することで既存のコードをほとんど変えることなくWebPを表示することができるようになりました。

注意した点

WebPを採用する上で、画質が本当に落ちないか、WebPにしたことで一部の画像が表示されないなどの問題が起きないかを特に気をつけていました。

料理の画像を表示するアプリケーションなので、画質が劣化することで料理がおいしくなさそうになってしまうととても残念です。そこでWebPとJPEGの画像を並べて表示し、極端な劣化が起きていないか検証するアプリケーションを作りました。社内でレシピ画像の見た目に変化がないか検証してからWebPを導入しています。

またWebPにしたことで低スペックな端末でレンダリングに時間がかかるようになってないかなども検証しました。通信量が減ってダウンロード時間が減ったとしてもそれの表示処理に時間がかかってしまっては意味がありません。ただ、これについては問題になりませんでした。

社内で検証して、これはいけそうだという話になっても、すぐにすべてを置き換えるようなことはしませんでした。検証したものの予期せぬ影響が見つかってしまった、そういう場合でもモバイルアプリはすぐに更新ができません。 そこで、WebP形式の画像配信を一部のユーザに対して行い、少しずつ対象をふやすような工夫をしていきました。幸い問題がおきなかったので、Androidはバージョン 4.3.1 から、iOSはバージョン 6.4.0 からすべてのユーザーを対象にするようになっています。

おわりに

サーバから取得した画像を多く表示するようなアプリの場合、画像のダウンロードにかかる通信量は無視できません。 WebPは画質をほとんど落とすことなくファイルサイズを減らすことができるので、ダウンロード待ち時間を減らしてユーザ体験を向上させる効果が期待できます。 サービス提供側にも通信量が減らせるメリットがあります。

少しずつでも重ねていけばこのような改善はアプリの性能向上に効いてくると思うので、色々試していきたいですね。


MacからiPhoneに遷移させよう

こんにちは。モバイルファースト室の中村(@_nkmrh)です。

突然ですが、Mac上で探したレシピをすぐiPhoneで見られると便利だと思いませんか?

先日リリースしたiOSクックパッドアプリではそれが出来るようになりました。

とても便利なのでぜひ活用してください。

※ 実はこの便利機能、次のバージョンで一旦取り下げ、問題を解決したあとで再度導入することになりました。以降の記事で事情を説明します。

  • Mac OS X YosemiteがインストールされたMac、iOS 8がインストールされたiPhone 5以降、iPad 第4世代、iPad Air、iPad mini、iPad mini Retinaディスプレイモデル、iPod touch 第5世代でご利用いただけます。
  • MacとiPhoneに同じiCloudアカウントを設定して下さい。

これがその様子...。

Mac上のSafariでクックパッドサイトを開いてレシピを探します。

すると...、iPhoneのロック画面の左下にクックパッドのアイコンが表示されます。

Image may be NSFW.
Clik here to view.

左下にクックパッドアイコンが...。

Image may be NSFW.
Clik here to view.

アイコンをスワイプすると...。

 

なんと...、Macで表示していたレシピがクックパッドアプリで表示されました...。 便利っ!!

Image may be NSFW.
Clik here to view.

このように便利なのですが、アプリリリース後、この機能には問題があることがわかりました。

Handoffの問題点

アプリがリリースされた翌日、12/05 の 0:00 に サーバーへのアクセスが急増し、捌き切れない状態になりました。 調査したところ、その1秒くらいの間に "/apple-app-site-association"というファイルに対して、普段の40倍くらいのアクセスがありました。

apple-app-site-association へのアクセスはインストール時に行われます。 インストールというのは 初回インストール時だけではなく、アップデートも含みます。 このタイミングはインストール or アップデートが終わったタイミングで、OS のデーモン (swcd) によってリクエストされます。

ところで、iOS にはアプリの自動アップデート機能があります。 というわけで、この自動アップデートが 0:00 に動き、一斉に /apple-app-site-association にアクセスが来ているのでは、と原因を推測しました。 アクセスは Apple 経由ではなく iOS 端末それぞれから来ているため、アプリのリリース後、毎回 0:00 にこのファイルに対して大量のアクセスが来る状況になりました。 (もちろん1端末1リクエストしかないので、トラフィックとしては大したことないのですが)

アップルのテクニカルサポートへこの件について問い合わせしたところ次のような返答がありました。

返答を要訳すると、アプリのインストール、再インストール、アップデート時に/apple-app-site-associationにアクセスします。自動アップデートの時間は決まってないけど、グラフを見ればリクエスト数予想できますね?アプリをアップデートするスケジュールに応じて、サーバーにどれくらいの負荷がかかるか予測して下さい。という旨のキビシい返答でした...

これに対して、リクエストの時間を分散させる等の対策をお願いしたところ、https://developer.apple.com/bug-reporting/にBug Reportを送ってほしいとの回答だったため、Bug Reportを送りました。

このような経緯のため、iOSクックパッドアプリの次のバージョンでは一度Handoff機能を取り下げ、大量リクエストに備えたチューニングをした上で来年再度導入することにしました。

ここまで長くなりましたが、それでもHandoff実装したい。という方へ、以降Handoffの具体的な実装方法等を紹介します。

iOS 8の新機能Handoff

HandoffはiOS 8とMac OS X Yosemiteからつかえるようになった機能です。Bluetooth Low Energy(以下、BLE)技術をつかい、iPhoneやiPad, Mac間でデータの送受信を実現しています。

※ BLEで一度に送受信できるデータサイズは、3KB以下です。それ以上のデータはストリーミング転送させることができます。

デバイスの設定

  • Handoffに対応するOSがインストールされたiPhoneやiPad, MacのiCloudアカウントを、同一のアカウントに設定します。
  • iOSでは設定.app > 一般 > Handoffと候補のAppからHandoffをONにします。
  • Macではシステム環境設定 > 一般からHandoffを有効にするチェックボックスをONにします。

実装

今回は上記で紹介したように、MacのSafariで開いているWebサイトからiPhoneアプリに遷移させる方法を紹介します。iOS間の実装も基本的には同じです。

apple-app-site-association

apple-app-site-associationファイルを作成します。 このファイルは、Webサイトのルートに配置しておくもので、連携するアプリのApp Idを記述したファイルをAppleが認可する証明書で署名したものです。

  • handoff.jsonファイルを作成し、以下の内容を記述します。

{"activitycontinuation":{"apps":["XXXXXXXXXX.com.example.myapp"]}} (XXXXXXXXXXの部分はApp Id Prefixを指定します。)

  • .p12ファイルをKeychain Access.appから書き出します。iPhone Developerの証明書を右クリックで選択し書き出しを選択します。

Certificates > iPhone Distribution: \<name\> xxx > Certificates.p12

  • opensslコマンドで証明書を作成します。

openssl pkcs12 -in Certificates.p12 -clcerts -nokeys -out output_crt.pem

  • 秘密鍵を作成します。

openssl pkcs12 -in Certificates.p12 -nocerts -nodes -out output_key.pem

  • 中間証明書を作成します。

openssl pkcs12 -in Certificates.p12 -cacerts -nokeys -out sample.ca-bundle

  • handoff.jsonファイルを下記のコマンドで署名します。

cat handoff.json | openssl smime -sign -inkey output_key.pem -signer output_crt.pem -certfile sample.ca-bundle -noattr -nodetach -outform DER > apple-app-site-association

  • apple-app-site-associationをWebサイトのルートに配置します。

Xcodeの設定

  • Info.plistにNSUserActivityTypesプロパティを追加します。TypeにArrayを指定、値をcom.example.${PRODUCT_NAME:rfc1034identifier}.activityTypeに設定します。(※ ActivityTypeは任意の文字列です。)
  • Target > Capabilities > Entitlements Associated Domainsを有効ONに、値をactivitycontinuation:example.comに設定します。

アプリの実装

iOS SDK 8.0からapplication delegate protocolとUIResponderクラスにHandoff用のメソッドが追加されています。

optional func application(_ application: NSApplication,willContinueUserActivityWithType userActivityType: String) -> Bool

application delegate protocol に追加されたメソッドです。引数のNSUserActivityオブジェクトの値を見て、Handoffの応答に応じるか無視するかを返すようにします。

optional func application(_ application: NSApplication, continueUserActivity userActivity:NSUserActivity, restorationHandler restorationHandler: ([AnyObject]!) -> Void) -> Bool

application delegate protocol に追加されたメソッドです。Handoffから起動した際に呼ばれます。引数で渡ってくるblockの引数にViewControllerのArrayを渡すことでViewControllerの-restoreUserActivityState:メソッドが呼ばれます。Handoffから起動した際の処理はこのメソッドに実装します。

func restoreUserActivityState(_ activity: NSUserActivity)

UIResponderクラスに追加されたメソッドです。このメソッドをViewControllerに実装します。Handoffで起動した際にNSUserActivityオブジェクトを受け取ることができます。

実装例

// application delegate

func application(application: UIApplication, willContinueUserActivityWithType userActivityType: String) -> Bool
{// trueを返した場合、 application:continueUserActivity:restorationHandler: が呼ばれます// falseを返した場合、 Handoffの応答を無視します// userActivityTypeはinfo.plistのNSUserActivityTypesプロパティに指定した文字列ですif userActivityType == "myType"{returntrue}returnfalse}

func application(application: UIApplication, continueUserActivity userActivity: NSUserActivity, restorationHandler: ([AnyObject]!) -> Void) -> Bool 
{// ここでuserActivityのURLを調べてHandoffで起動するかどうか判定しますvar urlString = userActivity.webpageURL?.absoluteString;
    if urlString == "xxxxx"{// block引数にmyViewcontrollerのrestoreActivityStateが呼ばれます
        restorationHandler([Viewcontroller])
        returntrue}returnfalse}
// ViewController

func restoreUserActivityState(activity: NSUserActivity)
{// Handoffから起動後に行う処理を実装します// Safariから起動した場合、webpageURLプロパティからSafariで開いているURLを取得できますiflet recipeID = activity.webpageURL?.lastPathComponent.toInt() {self.showRecipe(recipeID)
    }}

おわりに

いかがでしょうか。以上の手順でWebサイトとアプリ間の連携を実現することができます。便利な機能ですので興味のある方はぜひ試してみてください。


参考URL

Androidアプリ開発で素早くフィードバックをえるためのライブラリを作りました

モバイルファースト室の山下(@tomorrowkey)です。

Image may be NSFW.
Clik here to view.
f:id:tomorrowkey:20141211162434g:plain

Androidアプリを開発していて、ふとした時に不具合を見つけたりしませんか。
クラッシュであればDeploygateやCrashlyticsなどでクラッシュレポートを送ることができますが、表示崩れを報告をするにはスクリーンキャプチャを撮ってメールアプリを開き、画像を添付して、送信する、といった手順が必要でなかなか面倒です 。
アプリを開発する側は不具合のあったスクリーンショットがほしい、不具合を報告する人は報告する手順がめんどうといったギャップを解決するためのライブラリを作りましたので、紹介します。

不具合報告する機能を作りました

冒頭のアニメーションgifで一通りの挙動を見ることができます。
このライブラリを使っているアプリを開くと通知領域に「不具合を報告する」という項目が増えます。 不具合を見つけた時にこの通知を選択するとスクリーンショットが撮影されメール作成画面が開きます。
不具合を報告する人はこのメールを送るだけで表示崩れなどの不具合を報告することができます。

メールが届きます

実際に受信したメールがこちらです。

Image may be NSFW.
Clik here to view.
f:id:tomorrowkey:20141212133658p:plain

スクリーンショットが添付されているので開くと表示されます

アプリのスクリーンショットなので、ステータスバーが表示されませんが、不具合報告するには十分なものが撮れます。

Image may be NSFW.
Clik here to view.
f:id:tomorrowkey:20141212133713p:plain

この機能をアプリで使うには

この機能はライブラリとして作りました。
https://github.com/cookpad/issue-reporter-android

不具合報告機能をつけたいActivityに、Fragmentを追加する処理を書き足します。
BaseActivity.javaといった基底クラスがあれば、それに書き足すだけでアプリ全体にこの機能を追加することができます。

MainActivity.java

@Overrideprotectedvoid onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  setContentView(R.layout.activity_main);

  String mailAddress = "support@example.com";
  String subject = "Report an issue";
  IssueReporterFragment.apply(this, mailAddress, subject);
}

簡単に原理を説明すると、ActivityのRootのViewであるDecorViewを取得して、描画するためのキャッシュ(Bitmap)を取得してそれをメールアプリにIntentで飛ばしているだけです。
なにも特殊なことをしていないため、Permissionの追加も必要ありません。

まとめ

素早くモバイルアプリを開発するためには、開発とテストとフィードバックのサイクルをいかに早く回すかが鍵となります。
このライブラリを使えばフィードバック時のコミュケーションコストを少し抑えることができるようになると思います。
ぜひご活用ください。

Dockerでffmpegもimagemagickも怖くないという話

クックパッド 広告事業部の大野晋一です。責任範囲は広告事業の純広告およびネットワーク広告の商品開発担当で、事業部にはそれぞれの売上でコミットしています。

この記事では、動画変換の仕組みにおけるDockerの活用について紹介します。

クックパッドは8月8日、iOS/Androidのブラウザにおいて動画クリエイティブを掲出する広告商品を公開しました。広告商品としての詳細はプレスリリーススライドを見ていただくのがわかりやすいのですが、本稿に関係する特徴としてスマートフォンのブラウザで自動的に再生が開始されるというものがあります。

スマートフォンのブラウザにおいては、現在のところ、動画を自動再生させることは出来ません。これはAppleやGoogleといったブラウザベンダが課している制約です。そこで、クックパッドでは、janiというライブラリを使い、特定の規則に基づいて作られた画像を、JavaScriptで"動かす"ことによって自動再生を実現しました。

今回の話はこの変換の仕組みである、janiConverterに関するものです。

動画変換の仕組み:janiConverter

変換のざっくりとした処理手順としては

  1. 動画から、fpsで指定された頻度で静止画を切り出す
  2. 切り出した静止画を一定のルールで縦につなぎ合わせる
  3. 2で得た画像群をブラウザから読み出すことの出来るストレージに配置する
  4. 3の配置に応じたDOMをHTMLとして出力する
  5. ブラウザが、DOMおよびjaniのJavaScriptを受け取ると、動画として再生が可能になる

というものです。ここで1ではffmpegを使い、2ではimagemagickを使います。つまり、janiConverterを動かすインスタンス上にはffmpegとimagemagickが必要です。

もう一度言うと、「ffmpeg」と「imagemagick」がインストールされたインスタンスが必要です。

このふたつはコンパイルとセットアップが面倒なソフトウェアの東西横綱といっても過言ではなく、いろいろあって(※)、Dockerを使うことにしました。cookpadで本番投入されたDockerはこれが初めてになります。

※: いろいろというのは、主に個人的な興味と、試行錯誤で開発機の環境を汚したくないというのと、インフラストラクチャー部との30分ほどの検討のことです。おそらくインフラストラクチャー部としてもDockerの最初の被験者を捜していたところに、そこまでパフォーマンスが問われなくて社内向け、という良い鴨が現れた格好なのではないかと思っています

Dockerの導入

とはいっても、導入は簡単で、Linuxでセットアップする際のコマンドを普通にDockerfileに書いていくだけです。ffmpegについては、既に公開されているcellofellow/ffmpegを使わせてもらっています。

使ってみて一番良いなと思ったのはこの「普通にDockerfileを書けばOK」というところで、例えば、アプリケーションコードがDockerに依存したり影響を受けたりすることが無いのはDockerの良さです。

Dockerのメリットとおすすめの適用場面

そして、動画変換の仕組みを運用していくなかで以下のようなメリットがあると思っています。

  • 新しいインスタンスを用意する際にffmpeg/imagemagickの面倒な準備が必要ない。jenkinsがリポジトリの更新をポールして常に最新の状態でdocker buildしてくれているので、インスタンスはそれをpullしてくるだけ
  • janiConverterではsidekiqを使っており、実際のエンコードをworkerが処理するが、複数のworkerを立ち上げることが容易に出来る。ここでもworker用のインスタンスはdocker pullするだけ。リビジョンさえ同じなら全てのworkerの環境が同じであることが保証される
  • Docker依存がアプリケーションコードや設定ファイルに入らない。いつでも他の手段に移れる

また、開発においても次のような良さを体験できました。

  • 複数worker環境でのテストが容易である。マシンリソースの許す限りひたすらdocker runして立ち上げてテストできる
  • 本番と完全に同じ環境が保証される。たまたま開発環境で動いているみたいなバグをテストしてつぶしやすい。実際に、OS XとLinuxでの挙動の違いによるバグを発見して修正できた(初歩的ですね……)。
  • アプリケーションのポータビリティがいやでもある程度保証される。環境依存がハードコードされないのは良い

今回のように、master-worker構成が必要な仕組みを作ったり、ファイルなどのOS依存が出やすいものを扱う場合には開発においてもメリットは大きく、お勧めできます。そうした特徴のない通常のアプリケーションであれば開発段階ではあまりメリットはなさそうです。

Docker自体を運用していくノウハウとか、大量のトラフィックやリクエストをさばくノウハウなどはまだまだ確立されていないという感触ですが、このあたりは、今後当社のインフラストラクチャー部からも共有されていくことでしょう。

Android開発を爆速にする10のコマンドラインスクリプト

モバイルファースト室の山下( @tomorrowkey )です。
みなさんはAndroidアプリをビルドするときに AndroidStudioの実行ボタンを押すのと、ターミナルでgradleコマンドを実行するのと、どちらを使っていますか。
クックパッド社内のAndroidエンジニアでもどちらを使うか好みが分かれるのですが、私はたいていターミナルでgradleコマンドを使っています。
AndroidStudioの実行ボタンだとビルドを途中で中止できないことがあるからです。コマンドであればcontrol+cでいつでも中止できるという気軽さからコマンドを好んで使用しています。
開発するうえでIDEなどのGUIツールはとても便利なのですが、実はコマンドを実行する方がはるかに早くストレスなく開発を進めることができることがあります。
今回は私が実際に使っている便利なコマンドラインスクリプトを10個紹介します。

注意

今回紹介するコマンドラインスクリプトのほとんどはOSXでしか動作しません。
Linuxでもほとんどは動くはずですが、動作検証していないため分かりません。
Windowsのみなさんごめんなさい。そのままでは動きません。 スクリプトを読み替えてバッチファイルを作れば動く可能性があります。

目次

  1. adbコマンドの実行時にデバイスを選択する
  2. logcatを見やすく表示する
  3. adbをコマンド1つで再起動する
  4. デバイスにインストールされているアプリをアンインストールする
  5. 深いフォルダにあるapkファイルをインストールする
  6. スクリーンショットを撮影する
  7. gradleコマンドを補完する
  8. adbコマンドを補完する
  9. genymotionを素早く起動する
  10. ビルドが終わったら音を鳴らす

adbコマンドの実行時にデバイスを選択する

複数のAndroid端末を同時につないで困ってませんか?それadb-pecoで選択できるよ! - クックパッド開発者ブログ http://techlife.cookpad.com/entry/2014/09/09/172449

複数のAndroidデバイスがコンピュータに接続されている時に、adbコマンドを使おうとするとmore than one device or emulatorというエラーメッセージが表示されますよね。
adb-pecoを使えば複数のデバイスが接続されている場合に端末を選択することができるので、あのメッセージを見てイラッとすることは二度とありません。

logcatを見やすく表示する

JakeWharton/pidcat
https://github.com/JakeWharton/pidcat

これはとても有名ですね。
Jeff Sharkeyという方がcoloredlogcatを作っていたのですが、JakeWhartonがより見やすく使いやすくアップデートしたスクリプトです。

そのまま使うと単色でインデントも効いていない見づらいlogcatですが

Image may be NSFW.
Clik here to view.
f:id:tomorrowkey:20141217174923p:plain

pidcatを使うとカラフルに見やすく表示されます。

Image may be NSFW.
Clik here to view.
f:id:tomorrowkey:20141217174936p:plain

adbをコマンド1つで再起動する

Android端末を接続したけど、認識されないという時があります。 adbを再起動することで認識される場合があるのですが、2つのコマンドを打ち込む必要があります。

adb kill-server
adb start-server

リターンを含めると33文字もタイプしなくてはなりません。
デバイスが認識されない時はけっこうハマって何度もこのコマンドを叩かないといけないことが多いので、予め1つのコマンドにまとめておくと便利です。

.bashrcに以下の一行を追記します。

alias restart-adb='adb kill-server; adb start-server'

これでコマンド1つでadbを再起動することができるようになります。

$ restart-adb
* daemon not running. starting it now on port 5037 *
* daemon started successfully *

ただ連続でコマンドを叩くエイリアスを作っているだけですが、ちょっとした時に楽できます。

デバイスにインストールされているアプリをアンインストールする

デバイスから特定のアプリをアンインストールしたい時があります。
アプリをアンインストールするには端末上で操作をしなければなりません。
設定アプリを開き、アプリ一覧からアンインストールしたいアプリを選択して、アンインストールボタンを選択し、アンインストールされるのを待てばアンインストール完了です。
1日に何度もやるようなものではないとはいえ、ステップがいくつもありとても面倒です。
最近のAndroidであればランチャーからドラッグアンドドロップでアンインストールする方法もあり、前述した方法よりは楽にアンインストールすることができますが、これでもデバイスの操作がひつようなので面倒です。

コマンド一発でアンインストールできるようにuninstallappというコマンドを使っています。

alias uninstallapp='adbp shell pm list package | sed -e s/package:// | peco | xargs adbp uninstall'

pmコマンドでインストールされているアプリ一覧が取得できるのと、adbからアプリをアンインストールできるコマンドがあるので、組み合わせてターミナルからアプリをアンインストールできるようにしました。

Image may be NSFW.
Clik here to view.
f:id:tomorrowkey:20141217174959g:plain

深いフォルダにあるapkファイルをインストールする

今度はコマンドでビルドとインストールをしましょう。プロジェクトルートにいるなら./gradlew installDebugを実行します。 開発を進め、何度かビルドと検証を繰り返していくうちに、アプリの初期状態から実行してみたいと思うことはありませんか。 一度アンインストールして再インストールしてしまえばよさそうですね。アンインストールにはさっき作ったuninstallappコマンドが使えそうです。
インストールする方法はどうでしょう。 この時に選択肢は2つあり、adb installコマンドを使って最後に作られたapkファイルをインストールをするか、もう一度./gradlew installDebugを実行します。
前者の場合はadb installの後にapkファイルを直接指定しなくてはなりません。apkファイルが出力されるのはプロジェクトルートから辿ると./app/build/outputs/apk/app-debug.apkとなかなか深い位置にあります。頑張って指定するのもいいですが、何度もやりたい作業ではありません。
後者の場合はタイプする量は少ないですが、もう一度ビルドを走らせないといけないのでインストールされるまで結構時間がかかります。
たださっきビルドしたapkをインストールしたいだけなのですが、両者もそれぞれデメリットがあり、どちらも選択したくありません。
私はこれらのデメリットを解決するためにinstallappというコマンドを作って使っています。

alias installapp='find ./ -name *.apk | peco | xargs adb install -r'

findコマンドでapkファイルを探し、それをpecoに渡して選択できるようにして、選択されたapkファイルをadbコマンドでインストールしています。
インストールしたいだけであればinstallappと打てば楽にインストールできるようになりました。

Image may be NSFW.
Clik here to view.
f:id:tomorrowkey:20141217175026g:plain

pecoで選択できるようにしているので、複数のapkファイルがあっても簡単に選択することができます。

スクリーンショットを撮影する

新しく組んだレイアウトの確認やプルリクエストを作る時などデバイスのスクリーンショットを撮影する機会はたくさんあります。
みなさんはスクリーンショットを撮影するときはどのようなツールを使っていますか。
おそらくDDMSのスクリーンショットを撮るアイコンをクリックしてスクリーンショットを撮り、Saveボタンを押して画像を保存していると思います。
わずか2ステップですが、それですらコマンドを作ることでターミナルで楽に処理することができるようになります。

alias screenshot='screenshot2 $TMPDIR/screenshot.png; open $TMPDIR/screenshot.png'

android-sdkにscreenshot2というコマンドが入っています。
これはコマンドラインでスクリーンショットを撮影するためのコマンドです。
このスクリプトではスクリーンショットを撮影してプレビューで開くように処理しています。
使い慣れたプレビューで開かれるので、回転や縮小などの処理も簡単に行えます。

gradleコマンドを補完する

gradleコマンドは長くなりがちなので、打つのがとても大変です。 例えばbuildVariantsを使ってstagingサーバーに向けたdebugビルドを作ろうとした場合こんなコマンドを打ちます。

./gradlew installStagingDebug

29文字もタイプしなければなりません。 私はよくinstallをtypoして不明なタスクが実行されようとしているとgradleに怒られます。
このコマンドはよく打つのでまだ覚えることができますが、crashlyticsにリリースビルドをアップロードしようとした場合はこんなコマンドを打ちます。

./gradle crashlyticsUploadDistributionRelease

めったに打たないコマンドなのに45文字もタイプしなければなりません。 こんな長いコマンド覚えてられません。

そんな問題を解決できるのがcompletionというスクリプトです。 completionはターミナルのコマンドを補完するスクリプトの通称で、bashを補完するbash-completionやgitを補完するgit-completionなどがあります。

実はgradle用に作られたcompletionがあり、これを使えばもう長いコマンドを覚える必要はありません。

Gradle tab completion for Bash. Works on both Mac and Linux.
https://gist.github.com/nolanlawson/8694399

Image may be NSFW.
Clik here to view.
f:id:tomorrowkey:20141217175104g:plain

adbコマンドを補完する

completionを使ってgradleが補完されるようになりました。
あまりコマンドの数は多くありませんが、adb向けにもcompletionが作られており、gradle同様補完することができます。

mbrubeck/android-completion
https://github.com/mbrubeck/android-completion

android-completionはコマンドの補完は当たり前ですが、特定のデバイスを指定する-sオプションも補完してくれます。

Image may be NSFW.
Clik here to view.
f:id:tomorrowkey:20141217175127g:plain

adb-pecoとすこし機能がかぶっていますが、先にデバイスの指定するか後にデバイスを指定するかの違いがあるので、お互いに使い分けることができると思います。

genymotionを素早く起動する

Genymotionを起動するにはまずGenymotion.appを起動してエミュレータを選択して起動するといったステップが必要です。 ただこれだけの手順であれば面倒でもなさそうですが、Genymotion.appの起動はとても遅いです。 ライセンスの認証や初期化処理などで10秒くらい待たなければなりません。 ただエミュレータを起動したいだけなのに、エミュレータ一覧の画面を開くためだけに10秒も待てません。

genymotion-pecoを使えば解決することができます。

sys1yagi/genymotion-peco
https://github.com/sys1yagi/genymotion-peco

genymotion_pecoというコマンドを打てばエミュレータ一覧が表示されます。選択するとエミュレータが起動します。 エミュレータ一覧にはpecoを使っているので例えば4.4.2と打てば4.4.2のエミュレータだけ絞り込むことができます。

Image may be NSFW.
Clik here to view.
f:id:tomorrowkey:20141217183124g:plain

ビルドが終わったら音を鳴らす

Androidのgradleビルドってとても長いですよね。
アプリの大きさやキャッシュの状況にもよるんですが、大きなアプリだと1~2分かかることも珍しくありません。
みなさんとても優秀なエンジニアなので、そんなスキマ時間でさえ情報収集に余念がないと思います。 バックグラウンドでビルドしているのをただ待ってるのもなんなので、ちょっとの間だけFacebookやTwitter覗きますよね。 私もついFacebookやTwitterが捗ってしまい、とっくにビルド終わってるのになかなか開発に戻ってこれないことが何度もありました。
そんな問題を解決するためにnotifier-pluginを導入しています。

tomorrowkey/notifier-plugin
https://github.com/tomorrowkey/notifier-plugin

このpluginをgradleに導入するとビルドやテストが終わった時に、Notificationに通知を表示したりsayコマンドで音声でビルド完了を教えてくれます。
設定次第ではmp3ファイルやwavなど再生することもできるので、好きな効果音に変えることもできます。
これで安心して情報収集に集中できますね。

おわりに

Androidアプリを素早く開発するためのコマンドラインスクリプトを10個紹介しました。
ただコマンドをそのまま使うのではなく、より便利に使えるスクリプトを書くと開発はずっと楽になります。
便利なスクリプトを活用して快適な開発環境を整え、爆速でAndroidアプリを開発しましょう!

【学生限定】エンジニア志望の方が抱いている素朴な疑問を解決する「Cookpad TechBar」開催します!

こんにちは、レシピ投稿推進室の勝間(@ryo_katsuma)です。

このたび、学生のみなさんが抱く疑問を、仲のよい友人と語らうような雰囲気の中で、解決していきたい。』という思いを込めて、クックパッドのエンジニアがみなさんの疑問を毎回LTでお答えしていく、イベント「Cookpad TechBar (クックパッドテックバル)」を開催します。

Image may be NSFW.
Clik here to view.

記念すべき第1回目は、2015/1/23 (金) に開催します。テーマは、

「クックパッドってレシピだけじゃないの?」

下記に一つでも当てはまるエンジニア志望の学生の方はぜひご参加ください。

  • 「クックパッドってレシピだけじゃないの?」と思っている
  • クックパッドのエンジニアとして働くことに興味がある
  • Web業界でエンジニアとして働くことに興味がある
  • クックパッドの開発者ブログを読んでいる
  • クックパッドのエンジニア勉強会に興味を持っている
  • でも、いきなり勉強会に参加するにはハードルを感じている...

イベント詳細

プログラム

  • オープニング
  • 会社概要LT/質疑応答
  • 各エンジニア社員のLT/質疑応答
  • 自由交流会(お酒とお料理の用意も予定しております)

※ ノンアルコールの飲み物もご用意いたします。

参加社員

  • 勝間 亮(レシピ投稿推進室)
  • 京和 崇行(料理教室事業部)
  • 多田 圭佑(ホリデー事業室)
  • 井上 寛之(トレンド調査室)

参加社員が決定次第、随時更新いたします!

日時

2015/1/23 (金) 18:30〜20:30 (18:15〜受付開始)

場所

クックパッド株式会社東京本社オフィス
アクセス:https://info.cookpad.com/location/

参加対象者

エンジニア志望の学生の方

歓迎

2016年度卒業予定の方

参加費

無料

持ち物

筆記用具(アンケートをご記入頂きます)

参加方法

こちらよりご応募ください (外部サイトに移動します)

締め切り

2015/1/19(月)18:00
※ 応募者多数の場合抽選とさせていただきます。

最後に

学生の皆さんのご応募、お待ちしています!!

Viewing all 733 articles
Browse latest View live