読者です 読者をやめる 読者になる 読者になる

iOS9 のリリースでクックパッドに起きたこと

こんにちは、技術部モバイル基盤グループの茂呂(@slightair)です。

モバイル基盤グループでは、クックパッドの iOS/Android アプリに関する様々な仕事をしています。

  • 不具合を抑え、品質を保ちながら安定してリリースサイクルを回せる環境づくり
  • アプリの開発者がサービス開発に専念できるように、コードリファクタリングやライブラリの整備
  • OSやライブラリ、開発ツールのバージョンアップに伴う調査・検証・対応

この記事にはiOS9がリリースされた結果、クックパッドのサービスに何が起き、どういう対応をしてきたかをまとめます。

Universal Links

iOS9 で Universal Links という機能が入りました。これは、Safari で開いた Web ページ中のリンクに対応したアプリが端末にインストールされていれば、アプリでリンク先のコンテンツを表示できるというものです。 うまく Web サイトとアプリが連携すれば検索結果などからアプリへスムーズに誘導できてよさそうなこの機能が、クックパッドのアプリの場合は困ったことになりました。

iOS9 beta がリリースされてすぐの頃、検証していると Safari でクックパッドサイトのリンクを開こうとするとアプリが起動する事に気がつきました。Universal Links の機能を使うような設定をしてないのになぜ…?

Universal Links の機能は AppDelegate の application:continueUserActivity:restorationHandler: メソッドの実装を必要とします。クックパッドのアプリは iOS8 から導入された Handoff に対応しており、このメソッドを実装していました。そして cookpad.com の Webサイトにも apple-app-site-association ファイルを配置しています。 あとから(といってもiOS9リリース直前、GMが出たタイミングに)わかったのですが、この apple-app-site-association ファイルに activitycontinuation フィールドがあり、かつ Universal Links のための applinks フィールドがない場合、applinks フィールドに [ "*" ] が指定されているものと見なすようです。 つまり、意図せず cookpad.com の全てのページが Universal Links に対応しているという状態になってしまっていたのです。アプリケーション側でもリンクを検証していなかったので、cookpad.com に Safari でアクセスすると常にアプリが起動する状態になってしまいました。

iOS9 beta の初期の頃は全てのリンクでアプリが開いていましたが、その後リリースされたあるバージョンからは https のリンクのみアプリへ遷移するように変わっていました。そのため、iOS9の不具合だったりするのかなと思いバグレポートを送りましたが、答えがわからないままiOS9 GMがリリースされてしまいました。

https のリンクは、ログインページやご意見をいただくフォームに使っていました。なので、ユーザがWebページからログインしようとしたときにアプリが起動します。ユーザはアプリが起動した意味がわかりません。疑問に思ったユーザがどういうことなの?と「ご意見」のリンクからメッセージを送ろうとした時にもアプリが起動してしまいます。さらに混乱を招きます。この流れは、ユーザにとって最悪の体験になってしまうでしょう。

結局、Handoff の機能を一旦落とすことがユーザの体験を保つために必要なことだと僕らは判断し、8.2.0 で無効にしています。サーバ上の apple-app-site-association ファイルを更新することでインストールされている端末でもアプリに遷移しないようにしようと考えました。ただ、apple-app-site-association ファイルを取りに来るタイミングもよくわからず(インストール時?)効果はありませんでした。このファイルには Handoff の実装時にも瞬間的に大量のアクセスがiOS端末からやって来て泣かされています(MacからiPhoneに遷移させよう)。

無効にした 8.2.0 も iOS9 リリース直前ということで審査待ち時間が長引いていて、iOS9リリース前に間に合いませんでした。そのため一部のユーザにご迷惑をお掛けしてしまったことを残念に思っています。 きちんと Universal Links への対応を行い、Handoff の機能も今後のアップデートで復活させたいと考えています。しかし、現状はこれらの機能の検証が困難という問題があります。

App Transport Security

App Transport Security(以下 ATS)はアプリのセキュリティを向上させるものです。以前の記事や弊社エンジニアのブログでも言及されているので説明は省きます。

後述する様々な理由によりクックパッドのiOSアプリではまだ Xcode6 系を使用しています(2015/10/21現在、v8.5.0 以降で使用予定)。 なので、まだ ATS の設定をしていないのですが、最初は API サーバのみ有効にしようと考えています。 API サーバはすぐ有効にできそうなことが分かっています。

iPad Multitasking

iPad の一部の端末のみ対象ですが、2つのアプリを画面分割して表示できる機能が入りました。これにより、様々な画面サイズで iPad 向けアプリが表示されるようになりました。

AutoLayout や SizeClass を利用し、複数の画面サイズにアプリがきれいに対応できていれば問題ないのかもしれませんが、クックパッドのアプリは残念ながらそうはなっていませんでした。今のiPad向けの画面はAutoLayoutなどの概念が登場する前にデザインし、実装されたためです。 Multitasking に対応するには画面デザインを根本から考えなおさなければいけないようで、すぐに対応するのは難しそうです。 そもそもAndroidもなんですが、電話機向け・タブレット向けと画面を分けるのは正しくない時代になっているんじゃないかと考えています。

Safari

iOS アプリだけでなく Mobile Safari で表示する Web サイトにも色々な影響がありました。

コンテンツブロッカー

クックパッドのサイトは、iPhoneの場合はスマートフォンWebサイト、iPadの場合はPC向けWebサイトを表示しています。コンテンツブロッカーの中には、広告を取り除くだけでなくWebサイトの表示を一部崩してしまうものもあるようです。 様々なコンテンツブロッカーがあるので、コンテンツブロッカーを導入した環境での表示崩れに全て対応するのは困難であると考えています。そのため、コンテンツブロッカー利用時の表示崩れ・機能不全に関するご意見を頂いても申し訳ないのですがサポート対象外と案内しています。

収益への影響に関しては、まずは計測をしてみようということになっています(広告ブロッカーの検知と計測について)。

キャッシュがおかしい?

iOS9 の Safari でクックパッドのサイトを開くと毎回ログインしないといけなくなってしまった、というお問い合わせを頂きました。検証してみると、Safari のページキャッシュの挙動がなんだか怪しい事がわかりました。

具体的には、Safari を一旦終了し、起動し直すと前回開いていたページが表示されるのですが、この時にリクエストは飛んでいないことがわかりました。 ページのキャッシュ設定によりリクエストが飛ばないことは正しいのですが、表示されている内容がどうもおかしい。このためにブラウザとしてはログインしている状態なのにも関わらず、ログアウトしているように見える状況があるようです。この現象はGoogle Chrome などの他のブラウザアプリでは再現しませんでした。iOS 8 以前の Safari でも再現しないことを確認しています。ページをリロードをすると問題なく使える状態になるのですが、不穏な動きです。

僕の環境では、クックパッドに限らず他のサイトでも古いページが表示される現象に何度か遭遇しています。 クックパッドではキャッシュ設定を見直すなどして様子を見ていますが引き続き調査をしています。

閲覧履歴を削除すると Cookie も消える

こちらも iOS9 からの仕様変更なのかなんなのか理由がわからず困っている問題です。 Cookie が消えるということはログインセッションも消えてしまうので、ユーザは意図せずログインセッションを消してしまうことになり、毎回ログインを求められるようになってしまいます。「履歴の削除の操作でログイン状態も解除されてしまうのでそのような操作は行わないようにしてください」と案内するのもなんだかおかしな状況で、どうしたらいいんでしょう。ログインが必要なサイトを運営している方々はみんな困っているんじゃないでしょうか。

Xcode7/iOS9SDK

iOS9 で入った新しい機能を使うためには Xcode7 を使う必要がありますが、場合によってはビルドそのものができなくなってしまったり、iOS9 SDK を使うことで描画が崩れたり動作が変わるものがありました。

Swift2

Xcode7 から Swift2 がサポートされました。そのため、Swift 1.2 で書かれたコードはまずコンバートする必要があります。 クックパッドからリリースしているアプリには Swift で書かれたものがいくつかあります。 このアプリを修正して新しいバージョンをリリースするためには Swift2 に対応する必要があります。

Xcode には Swift2 への移行ツールが付属しているので、だいたいはそれで移行できます。 ただし、利用しているライブラリが Swift2 に対応しているか、対応していてもライブラリの修正にアプリを追従させなければいけないなどの問題があります。各プロジェクトで移行を進めている段階です。

言語仕様変更に追従していく大変さはありますが、どんどん便利になっているので Swift を採用していきたい気持ちはとても大きいです。

UIApplication canOpenURL: が使えなくなった

iOS9 から指定したカスタムURLスキームが開けるかどうか確認する canOpenURL: が何も設定しないと常に false を返すようになりました。アプリ間連携のために遷移先のアプリがインストールされているか調べるために必要なメソッドでした。これはクックパッドでアプリ間連携するアプリのリストを作り、Info.plist に指定するだけで問題なさそうです。

iOS9SDK でビルドすると見た目が変わる・描画が崩れる

アプリを実行する端末のOSバージョンと、アプリをビルドするSDKのバージョンの組み合わせで、描画内容が変わったり、動作が微妙に異なるケースがあることに気が付きました。クックパッドのテスト環境では、テストケースを回しながら画面のスクリーンショットをとり、その差分を検出する仕組みがありました。そのため、新しいSDKを使った場合にどのような変化がアプリに起こるか網羅的に確認する事ができました。

iOS9 でフォントが変わったのもありますが、それ以外にもコードの記述やその他の理由で以前のSDKでは発生しなかった不具合を見つけられたので、その対応が終わるまで新しいSDKの使用を延期するという手をとることができました。

iOS9 のみで起きるクラッシュ

クックパッドのアプリでは fabric.io を利用してアプリのクラッシュ数・クラッシュ原因を収集しています。iOS9 がリリースされてから iOS9 のみで発生するクラッシュの数が多いようで悩んでいます。内容を見てみると、iOS の UIKit のとあるメソッドで例外が発生しているようです。デベロッパにはどうしようもない類のこのクラッシュが他のクラッシュの20倍くらいの数で起きているようなので、早めにこの不具合が修正されることを望んでいます。

今後に備えて対応したもの

困ったことばかり書いていてもつらいだけなので、今後に向けて対応したもの、対応していこうとしているものも書きます。

App Thinning

ユーザが端末にアプリをインストールするときに、必要なリソースのみをAppStoreからダウンロードできるようになる仕組みが入るようです。例えば、iPhone6 でアプリが動作するときには @2x の画像リソースが使われるので、@1x, @3x のリソースは必要ないはずです。そのような状態で @3x の大きな画像までダウンロードしないといけないのは無駄です。

画像リソースをきちんと AssetCatalog で管理するようにすればよいとのことだったので、つい最近重い腰を上げて修正しました。クックパッドのアプリではたくさんの画像を使っているので、大変な作業になるのではないかと躊躇していましたが、意外とすんなり移行することができました。ついでに不要な画像を取り除くこともできたのでいいことづくしでした。

Bitcode というコンパイルしたプログラムの中間表現を作成し、アップロードすることで、AppStore が最適化したプログラムを配信できるようにするという仕組みも App Thinning の一部として追加されました。 ただ、こちらは依存しているライブラリの対応をそれぞれ待つ必要がありそうなので、まだクックパッドではしばらく有効にできないなと判断しています。

Nullability, Lightweight Generics

Xcode6 で Nullability, Xcode7 で Lightweight Generics が Objective-C に導入されています。コードがわかりやすく、安全になるだけでなく、Objective-C のプロジェクトの Swift への移行のサポートにもなるので、積極的にコードを修正する際には付けて回りたいと考えています。

まとめ

iOS9 がリリースされたことでクックパッドのサービスに起きたこと、それにどう対応したか、どう対応していくかについて書きました。 まだ対応が完了せずに調査を進めているものもありますが、サービスを安定して続けるために、新しい価値をとり込んだり作っていくために、日頃からアンテナを高くして変化に対応していかなければなりません。

なかなか外的要因で難しくなってしまう場合もあるのですが、一番はユーザのみなさんに不都合なく安定したサービスを続けていけることだと思っています。また、新しい体験を使ったより良いサービスを届けられるよう、努力していきたいと考えています。

/* */ @import "/css/theme/report/report.css"; /* */ /* */ body{ background-image: url('http://cdn-ak.f.st-hatena.com/images/fotolife/c/cookpadtech/20140527/20140527163350.png'); background-repeat: repeat-x; background-color:transparent; background-attachment: scroll; background-position: left top;} /* */ body{ border-top: 3px solid orange; color: #3c3c3c; font-family: 'Helvetica Neue', Helvetica, 'ヒラギノ角ゴ Pro W3', 'Hiragino Kaku Gothic Pro', Meiryo, Osaka, 'MS Pゴシック', sans-serif; line-height: 1.8; font-size: 16px; } a { text-decoration: underline; color: #693e1c; } a:hover { color: #80400e; text-decoration: underline; } .entry-title a{ color: rgb(176, 108, 28); cursor: auto; display: inline; font-family: 'Helvetica Neue', Helvetica, 'ヒラギノ角ゴ Pro W3', 'Hiragino Kaku Gothic Pro', Meiryo, Osaka, 'MS Pゴシック', sans-serif; font-size: 30px; font-weight: bold; height: auto; line-height: 40.5px; text-decoration: underline solid rgb(176, 108, 28); width: auto; line-height: 1.35; } .date a { color: #9b8b6c; font-size: 14px; text-decoration: none; font-weight: normal; } .urllist-title-link { font-size: 14px; } /* Recent Entries */ .recent-entries a{ color: #693e1c; } .recent-entries a:visited { color: #4d2200; text-decoration: none; } .hatena-module-recent-entries li { padding-bottom: 8px; border-bottom-width: 0px; } /*Widget*/ .hatena-module-body li { list-style-type: circle; } .hatena-module-body a{ text-decoration: none; } .hatena-module-body a:hover{ text-decoration: underline; } /* Widget name */ .hatena-module-title, .hatena-module-title a{ color: #b06c1c; margin-top: 20px; margin-bottom: 7px; } /* work frame*/ #container { width: 970px; text-align: center; margin: 0 auto; background: transparent; padding: 0 30px; } #wrapper { float: left; overflow: hidden; width: 660px; } #box2 { width: 240px; float: right; font-size: 14px; word-wrap: break-word; } /*#blog-title-inner{*/ /*margin-top: 3px;*/ /*height: 125px;*/ /*background-position: left 0px;*/ /*}*/ /*.header-image-only #blog-title-inner {*/ /*background-repeat: no-repeat;*/ /*position: relative;*/ /*height: 200px;*/ /*display: none;*/ /*}*/ /*#blog-title {*/ /*margin-top: 3px;*/ /*height: 125px;*/ /*background-image: url('http://cdn-ak.f.st-hatena.com/images/fotolife/c/cookpadtech/20140527/20140527172848.png');*/ /*background-repeat: no-repeat;*/ /*background-position: left 0px;*/ /*}*/