本番/ステージング環境GPUぼくめつ大作戦

機械学習チームの林田(@chie8842)です。好きなスポーツはテニスとスノボです。

システムは、その当時の最新の技術で作ったとしても必ずレガシー化します。 機械学習システムも他システムと同様、一度デプロイしたら終わりではなく、継続的なメンテナンスが必要です。昨今機械学習は、特に技術の進歩が目覚ましいため、レガシー化するのも早い分野といえます。本稿ではレガシー化した機械学習アプリケーションのメンテナンスと、それに伴うGPU環境からCPU環境への移行によって、大幅にシステムの運用コストを削減した例をご紹介します。

機械学習アプリケーションにおけるコスト課題

クックパッドにおける最初の大きな機械学習プロジェクトである料理きろくがリリースされたのは、2年前のことです。それ以来、様々な機械学習アプリケーションがデプロイされ、現在では大小含めて30を超える機械学習アプリケーションが運用されています。そのうち、10 個のアプリケーションがGPUサーバ上で運用されており、常時数十台のg2.2xlargeインスタンス1が起動している状況でした。

ここで、これらのGPUインスタンスについて、コスト面における問題が2つありました。 1つは実行環境であるEC2インスタンス料金です。GPUインスタンスは他のGPU非搭載インスタンスと比べて単位時間あたりの使用料金が非常に高価です。たくさんのGPUインスタンスが稼働していることで、クックパッド全体のサーバコストを逼迫させているという問題がありました。 2つめはメンテナンスにかかる人的コストです。GPUサーバをアプリケーション環境として利用することで、通常のモニタリングメトリクスに加えてGPUのリソース監視が必要になります。さらに、コンテナからデバイスドライバを使用するための権限設定など、特別な対応が必要となり、将来的にも人的メンテナンスコストがかかるという問題がありました。

機械学習における処理パフォーマンス

機械学習には、データの特徴をよく表すモデルを作る「学習」と呼ばれる過程と、 作ったモデルを使って分類問題を解くなどの「推論」と呼ばれる過程に分けることができます。 例えば、犬と猫の画像を使って、犬と猫を分類するモデルを作るのが「学習」過程、そのモデルを使って犬と猫の画像を分類するのが「推論」の過程です。

クックパッドでは、GPU環境を利用して定期的にモデルを再学習しているアプリケーションはなく、GPU環境のアプリケーションは、全て「推論」に利用していました。

GPUには、並列処理を高速化できるという特徴があります。機械学習の中でも、特にディープラーニングなどのニューラルネットワークを用いる場合に、GPUが用いられることはよく知られていますが、顕著に処理パフォーマンス上効果的なのは、以下の場合です。

  1. ディープラーニングにおける「学習」処理2
  2. 非常に高いパフォーマンスが要求される「推論」処理

実はディープラーニングを使った機械学習アプリケーションでも、GPUが必要なケースというのは、意外と限られているのです。 例えば、Applied Machine Learning at Facebook: A Datacenter Infrastructure Perspectiveという論文では、以下のように、機械学習システムを大規模に利用しているFacebookのサービスでさえ、推論にはCPU環境を利用しているということが、語られています。

Facebook currently relies heavily on CPUs for inference, 
and both CPUs and GPUs for training, 

クックパッドでは、モデル学習にGPUを使っていますが、先に書いたとおり、アプリケーションとして実行しているものにおいて、1.に当てはまるものはありません。また、ユーザに対してレスポンシブに応答を返したり、大量のデータを処理したりするといったものはないため、2.についても回避できる可能性が高いということが分かっていました。 そこで、必要なものはアプリケーションのチューニングを行い、GPUアプリケーションを全てCPU環境に移行することにしました。

CPU環境への移し替えに伴うサービス影響確認とパフォーマンス・チューニング

GPUで動作しているアプリケーションをCPU環境に載せ替えるにあたり、まずはCPU環境への移行によるパフォーマンス影響の調査を行いました。 Webシステムにおけるパフォーマンスの重要な指標として、「スループット」と「レスポンスタイム」があります。 スループットとは、一定の時間内に処理できるトランザクション数、レスポンスタイムとは、クライアントからのアクセスに対しての応答速度です。 目標とするスループットやレスポンスタイムが決まっていれば、それらにあわせればよいですが、今回は目標が明確に決まっていないアプリケーションが多い状態でした。そこでまずはGPUを利用した場合とCPUを利用した場合のパフォーマンスの差を測定し、それをもってプロダクトオーナと協議するという方式をとりました。

以下の節では「料理きろく」3という写真に対して料理/非料理判定を行う機械学習アプリケーションの場合を例にとって、パフォーマンス計測とチューニングの進め方を紹介します。 料理きろくの詳細については以下をご覧ください。

techlife.cookpad.com

techlife.cookpad.com

チューニング前のパフォーマンス

料理きろくの機械学習部分は、TensorFlow v1.2.1で実装されていました。 GPUインスタンス上で、アプリケーションの推論部分のみをtensorflow-gpuパッケージとtensorflowパッケージ[^3]で動作させた結果、以下のとおり、tensorflow-gpuを利用したほうが約18倍速いことがわかりました。

GPU CPU
レスポンスタイム 0.0723秒/枚 1.2776秒/枚

なお、上記レスポンスタイムは、下記のように100枚の推論の平均値をとっています。

elapsed_times = []
for i in image_list:
    image = Image.open(i)
    start = time.time()
    model.infer(image) # 推論処理
    elapsed_time = time.time() - start # 実行時間
    elapsed_times.append(elapsed_time)

print(mean(elapsed_times)) # 実行時間の平均値の表示

TensorFlowのチューニング

まずはTensorFlowのレイヤーで、レスポンスタイムを縮めるようチューニングしました。 料理きろくのために行ったチューニングの中では以下の2つの項目が効果的でした。

  1. TensorFlowバージョンアップ
  2. tf.Sessionを使い回すようにする

順に説明します。 まず1.についてです。TensorFlowは、非常に開発が活発なソフトウェアです。バージョンが上がるごとにTensorFlow自体の高速化も行われているため、単純にTensorFlowのバージョンを上げることで、パフォーマンスが向上することが予想できました。このため、今回は思い切ってTensorFlowのバージョンを1.2.1から最新の1.12.0に上げました。バージョンアップに伴いリファクタリングは必要でしたが、モデル自体はv1.2.1で学習したものをそのまま使うことができました。

次に2.についてです。TensorFlowでは、定義した内容を実行するために、tf.Sessionというセッションが必要です。 このtf.Sessionの起動にはオーバヘッドがあるため、複数の処理ごとに張り直すと、それだけで処理が非常に重くなります。そのため、1つのセッションをできるだけ使い回すように修正しました。

元のコードの書き方

    def hoge():
        with tf.Session as sess:
            sess.run()


    def fuga():
        with tf.Session as sess:
           sess.run()

セッションを使い回す場合の書き方

sess = tf.Session()

def hoge(sess):
    sess.run()

def fuga(sess):
    sess.run()

(省略)

sess.close()

その結果、元のCPU版と比べてレスポンスタイムが5分の1程度になりました。

GPU CPU(チューニング前) CPU(チューニング後)
レスポンスタイム 0.0723秒/枚 1.2776秒/枚 0.25秒/枚

Dockerコンテナリソースのチューニング

TensorFlowレイヤーにおけるチューニングはここまでとして、次に実際本番環境と同等のCPU環境で動作させる場合のアプリケーションとDockerコンテナリソースをチューニングしました。 CPUで動作させる場合の本番環境は、c5.xlargeインスタンスからなるクラスタを利用します。4 そのためc5.xlarge環境で、リソースをモニタリングしながら、アプリケーションを実行します。 今回の検証ではリソースモニタリングツールとしてsarを利用しました。sarを採用した理由は、非常に軽量で扱いやすく、時刻付きのテキスト形式でログを保存しやすいためです。

結果として、Dockerコンテナ上で、ホストのリソース利用制限を行わずにアプリケーションを実行した結果、レスポンスタイムとリソース使用率は以下のようになりました。

レスポンスタイム CPU使用率 メモリ使用率
0.474秒/枚 93%程度 20%程度

TensorFlowは、処理が1プロセスであっても、デフォルトで使用可能なCPUすべてを使います。 1プロセスでCPUをほとんど使ってしまうので、1ホスト上で実行するアプリケーションのプロセス数やスレッド数を増やしても、レスポンスタイムが低下してしまい、スループットもそれほどあがらないだろうということがわかります。このため、1ホスト1コンテナ構成で、スループット確保のためには、スケールアウトを行うしかない、という結論となりました。

移行

上記の実験から、GPUからCPUへの移行のパフォーマンス影響としては、以下のとおりとなりました。

  • レスポンスタイムがGPUを利用した場合と比べて0.47秒程度増える
  • スループットはサーバのスケールアウト(オートスケール)により担保する

この内容でプロダクトオーナーに了解をとり、移行を行いました。 クックパッドでは、Hakoという、Kubernetesライクなコンテナオーケストレーション環境が導入されており、移行自体は、Dockerイメージの更新及びHakoの定義ファイルの更新のみを行えば、スケールアウトも自動で行われ、サーバ作業を行うことなく実行できます。 Hakoについては、詳しくは以下の記事を参照してください。

techlife.cookpad.com

最初はGPU環境と並行運用し、最終的にCPU環境のみを残すようにして、移行します。

コスト削減の結果

現在移行による並行運用中のアプリケーションもありますが、料理きろくも含めてすべての移行対象アプリケーションをCPU環境で動作しています。

これによるサーバコスト削減効果を算出したところ、EC2利用料金は元の6分の1程度となり、年間1,500万円以上の節約となることがわかりました。

その他の有効なチューニング

今回はチューニングの一例として、料理きろくをCPU環境に移行する内容を紹介しました。 最後に、料理きろくのチューニングではやらなかったけどその他の一般的に有効なチューニング手法を3つだけ紹介します。

モデルアルゴリズムの変更

料理きろくのモデルは、InceptionV3ベースです。モデル更新を行う必要があるため、今回はしませんでしたが、最近では、MobileNetなどの軽量モデルが発展しており、こうしたモデルに置き換えることで、パフォーマンスが向上する可能性が大きいです。

プロセス数/スレッド数の調整

料理きろくでは、TensorFlowの演算によるCPUがボトルネックとなったため実施しませんでしたが、1プロセスでCPU、メモリ、IOといったリソースを十分に使い切れていない場合、WSGI等を利用してプロセス数、スレッド数を増やすことで、1ホストあたりのスループットをあげることができます。

共有メモリの利用

上記において、例えばプロセス数を増やした場合、各プロセスがメモリ上にモデルをロードすることになります。現状クックパッドで運用しているモデルには、それほど巨大なモデルがありませんが、例えば今後以下ブログにおいて紹介したBERTなどの巨大なモデルを利用したいとなったときは、プロセス間で同じ共有メモリ上のモデルを利用するようにアプリケーションを記述することが有効になるかもしれません。

techlife.cookpad.com

さいごに

機械学習というと、新しい手法を試し、ハイパーパラメータチューニングを行ってモデルの精度を高めることに興味がある人が多いでしょう。しかしながら、機械学習をサービスに活かすためには、こうしたアーキテクチャ面における「チューニング」も重要な仕事であることを分かっていただけたなら嬉しいです!


  1. AWS上のGPUサーバ

  2. 最近はGPUが必要ない軽量なモデルもある

  3. tensorflow-gpuはGPU版、tensorflowはCPU版のPythonのTensorFlowパッケージ

  4. 厳密にはc5.xlarge以上のリソースを持つインスタンスタイプが混ざったスポットインスタンスからなるクラスタ

毎週リリースを実現するテスト活動

こんにちは。 品質向上グループの茂呂一子(@ichiko_revjune)です。

クックパッドアプリは、サブミット・リリース作業を自動化して、アプリを毎週サブミットするようになりました。これを実現するリリースフローについては、 クックパッドアプリはみんなが寝ている間にサブミットされる で紹介しました。

このリリースフローを実現していく過程では、「機械に人間があわせる」という方針で、サブミット・リリース作業が自動化されていきました。つまり、毎週サブミット・リリースをするためには、何をどのように自動化するべきかという視点で自動化する対象が決まっていきました。

アプリは開発が終わればすぐにリリースできるというものではありません。この記事では、リリース前のテスト作業をどのように調整して、毎週リリースを実現しているのかを説明していきます。自動サブミットの導入はiOSアプリが先行したため、ここではiOS版クックパッドアプリについて説明します。

自動サブミット導入以前のリリース前テスト

この記事 にあるように、以前のリリースフローでは、開発期間、コードフリーズ、テスト期間を経てリリースをしていました。このテスト期間に行うテストをリリース前テストと呼びます。テスト期間は、3〜4営業日でした。

リリース前テストの内容は大きく3つです。

ひとつめは、UI操作を伴うシナリオテストを複数のテスト環境(iOSデバイスとiOSバージョンの組み合わせ)で実行することです。このシナリオテストは、Appiumを使って機械的に実行しています。

ふたつめは、自動化がむつかしいが重要な機能を手動でテストすることです。例えば、In App Purchase に関する機能や、写真撮影が必要な機能などです。

みっつめは、各リリースに含まれる変更に関連した機能を手動でテストすることです。変更内容から起こりそうな不具合を想像し、それを実際に確認するというような作業です。探索的テストと呼ばれるものに近いでしょう。

これらを実施して不具合が見つかれば修正し、リリース前にその修正がうまくいっているかを確認していました。

自動サブミット導入のために

自動サブミット導入後もリリース前テストはなくなりません。スケジュールされた自動サブミットが完了し、テスト対象が確定してからリリース前テストが始まります。

先に説明した、以前のリリースフローにあわせて設計したテストをそのまま実施すると3〜4営業日かかることになり、リリース前テスト以外の活動にあてられる時間がなくなってしまいます。わたしたちは自動化したシナリオテストを持っているため、テスト実施以外にも、これらをメンテナンスする時間は必要です。また、テストツールを改善したり、リリースフローを改善するなどの活動もリリースを続ける上では欠かせません。テストのやり方か内容を変更しなければ、毎週のサブミットに耐えられなくなってしまいます。

自動化できるところは自動化し、人間にしかできないことを人間が行うという方針で、テスト作業を効率化してきました。自動サブミット導入当初はサブミットからリリースまで、2〜3営業日かかっていました。しかし、最近は2営業日でリリースできるようになってきました。

幸いにして、大きな不具合を出すことなく、毎週のサブミットとリリースが実現できています。テスト作業をどのように変えていったのかを課題ごとに説明していきます。

一番時間がかかるのはシナリオテスト実行

既に自動化してあった200件弱のシナリオテストは、ひとつのテスト環境(iOS デバイスとiOSバージョンの組み合わせ)での実行に10時間以上かかっていました。加えて、以前はシナリオテストをCIマシン上で実行させる為の環境整備が追いついておらず、手元のPC上で実行していました。そのため、毎晩の就業時間外にひとつのテスト環境を選び、実行していました。

リリース前テストでは、iPhone/iPadいずれの環境でも動かすことと、サポートiOSバージョンを網羅することを満たすよう、複数のテスト環境で実行します。したがって、1リリースあたりのシナリオテストにかかる時間は、10時間かけるテストする組み合わせの数でした。

これらの問題はCIマシンでの実行と、シナリオテストの並列実行という方法で実行時間を短縮しました。

今年に入ってから、CIマシン上で実行することは試みていました。そして、自動サブミット導入の頃に準備が整い、本格的に実行をCIマシンに任せるようになりました。シナリオテストを実行しているCI環境はビルド用のCI環境とは別のものを使用しています。理由はふたつあり、ひとつめは、実行にかかる時間が長いため短時間で終了するビルドタスクと混ぜて実行するべきでないためです。ふたつめは、最新のOSでは動作が不安定になりやすく、ビルド環境と同じタイミングでOSを更新できないことが多いためです。

シナリオテストの並列実行は、今年チームに加わった加藤が、実行の高速化を目的に実現してくれました。並列実行と呼んでいるのは、同じデバイスとiOS を使用するiOS Simulatorを複数起動し、別のシナリオを同時に実行することです。UIテストのボトルネックのひとつにSimulatorを操作する時間があるため、Simulatorを増やすことに効果がありました。これによって、1つのテスト環境でのテスト実行にかかる時間が約3分の1になり、実行時間の総量も同じように少なくなりました。

実行時間の短縮という他にも、金曜日から日曜日にかけてシナリオテストを実行するようスケジュールして、実行開始が遅延してもシナリオテストが終わるのを待つ時間が発生しにくいように工夫しています。自動サブミットは金曜日の早朝に動きだすので、順調にいけば金曜日の午後にはシナリオテスト結果がそろい、レポートを見れるようになっています。

シナリオテスト実行の指示を毎回人間がしていた

シナリオテストは、1回のリリースのために複数のテスト環境で実行しています。テスト環境は固定しておらず、リリースごとに変え、ある程度の期間でみたときに、iOSデバイスサイズとiOSバージョンを網羅するように設計してきました。

自動サブミット導入以前は、このテスト環境の選定を人間が行い、実行のたびに指定していました。

自動サブミット導入後は、このテスト環境の選定をスクリプト化し、実行のたびに指定しなくて済むようにし、機械による無作為抽出を導入しました。

機械に任せる領域を増やして、リリース前テストの準備をひとつ減らしました。

お手本画像を人間が更新していた

シナリオテストツールには、表示くずれを見つけるために、シナリオテスト中で取得したスクリーンショット画像をお手本画像と比較して、差分を表示する機能があります。

このお手本画像の管理には2つの課題がありました。ひとつめは、テストツールの実行環境ごとにファイルを管理しており、すべての環境で同じファイルを使っている保証がなかったことです。ふたつめは、人間が差分画像を見ながら更新すべきファイルを探し、コピー&ペーストでお手本画像を更新していたことです。実行環境が複数存在したため、特定の環境への反映漏れということも起きていました。

この作業を改善するにあたって、まず、お手本画像はAWS S3に一元管理するようにしました。実行のたびに、S3から最新のお手本画像を取得し、どこでも同じお手本を使用するようにしました。

次に、更新作業の一部をスクリプト化し、更新するべきかの判断だけを人間が行うようにしました。人間は差分画像の一覧をみながら、更新する画像にマークをつけます。その選択結果をファイルに出力し、それをスクリプトに与えて実行すればS3上にファイルが送信されます。

単純作業を機械に任せることで、人間は不具合の探索に集中しやすくなりました。

シナリオテスト結果から不具合をみつける手助け

シナリオテストは、UI操作を伴うため、不具合がなくてもシナリオが失敗することがあります。本当に不具合があるのかは最終的に、人間が実機を操作して判断することになります。その失敗が不具合の可能性が高いのか、低いのかを知ることで、この再操作を減らすことができます。

失敗しやすいシナリオや不安定なシナリオを見つける目的で、シナリオごとの実行結果を蓄積、可視化するアプリケーションを構築しました。このアプリでは、同じバージョンのアプリに対する複数のテスト環境での実施結果の成否を一覧できます。「どのテスト環境でも失敗しているので不具合の可能性が高い」「ひとつの環境だけ失敗しているので不具合の可能性が低い」といった具合に参考情報を簡単に見つけることができるようになりました。

f:id:ichiko_revjune:20181212111114p:plain
シナリオごとの実行結果を蓄積、可視化

このアプリがない頃は、複数のレポートを開いて見比べていました。成功失敗というデータに絞って可視化することで、ずいぶんと容易に理解できるようになりました。

自動化できない手動テストの効率化

自動化することはできないけれど重要な機能、例えば In App Purchase に関する機能は毎回手動でテストをしています。リリースごとの追加機能を対象とするテストではないので、ほぼ内容は決まっています。テスト作業の見直しを始めた時点では、削ってしまえる機能もありませんでした。

これらの手動で実施するテストの項目は、厳密なテスト手順を定めておらず、この機能でなにができること、という満たすべき項目をリストにしています。探索的テストのチャーターのようなものでしょうか。

自動サブミット導入以前も、関連機能に変更がなければ省略してもよい項目はあり、省略することもありました。自動サブミット導入後は、これを厳格に適用して、関連機能に変更がなく不具合が発生しないであろう機能へのテストを省略するようにしました。合理的にテストしすぎない工夫としては、うまくいっている方ではないでしょうか。

これからの課題

小さな改善を重ねてリリース前テストに時間をかけ過ぎることなく、毎週のリリースを実現している方法を紹介しました。

サブミット自動化後のリリースフローは、リリース前テストで致命的な不具合が見つかった場合、リリースをあきらめるルールです。このフローがうまくまわっていくためには、不具合が入りにくいしくみや、機能変更をマージする前に不具合に気づきやすくするしくみが必要です。そのためには、開発チーム内のテスト技術を向上させることも必要でしょう。これらの点ではまだ効果的なアクションを取れていないので、急がず確実に小さな変化を積み重ねていきたいです。

iOSでの読みやすい幅

モバイル基盤グループのヴァンサン(@vincentisambart)です。

iOSの設定画面の右側は一定の幅を超えないように作られています。

  • iPadでは: iPadでの設定画面

  • 新iPad Pro 12.9"では: iPad Pro 12.9インチでの設定画面

iPadでTwitterのタイムラインのセルの中身も一定の幅を超えません。

iPad Twitter

このように、自分のアプリで広い画面でもコンテンツが広がりすぎないようにするためにはどうすればよいのでしょうか。AutoLayoutでいくつかの制約を使ってできるのですが、もっと簡単な方法はないのでしょうか。

iOS 9以上では、端末の種類を気にせず、複雑なAutoLayout制約を使わず、殆どのビューですぐ使える仕組みがあります。Appleのドキュメントで「readable content」や「readable width」と呼ばれているものです。以下日本語で「読みやすい幅」と呼ぶことにします。

注意点:下記の説明はAutoLayoutを使う前提で書かれています。AutoLayoutを使わない場合、親ビューのreadableContentGuidelayoutFrameで読みやすい幅の明確な数字が取れるので、それを元にレイアウトを計算することになるのではないでしょうか。

(Twitterは読みやすい幅機能が使える前からこの表示でしたので、実際独自で同じことを実装しているようです。)

読みやすい幅とは

文章の表示領域の幅が広がりすぎると、少し読みづらく感じてしまいませんか。iOS 9以上では、readableContentGuideを使って幅が広がりすぎないようにできます。ビューのレイアウトの制約を定義する時、親ビューのleadingAnchor/trailingAnchor/leftAnchor/rightAnchorlayoutMarginsGuideの代わりにreadableContentGuideを使います。(因みに読みやすい幅のためのガイドなので、readableContentGuideの垂直方向のアンカーtopAnchorbottomAnchorcenterYAnchorheightAnchorが実質layoutMarginsGuideのと同じです)

縦向きのiPhoneにしか表示されないコンテンツは読みやすい幅を使うメリットがあまりないのですが、iPadや横向きのiPhoneでも表示されるコンテンツは画面の全幅に渡ると少し読みづらくなるので読みやすい幅を適切に使えば良いではないでしょうか。

因みに「読みやすい」といっても、テキストの幅を合わせるだけでなく、セルに入っている画像やボタンの配置も読みやすい幅に合わせたほうが自然だと思います。

「読みやすい幅」は具体的にどういう幅なのでしょうか。全画面のroot view controllerのビューに子ビューを貼って、その子ビューの制約を親ビューのreadableContentGuideに合わせて、子ビューの幅を測ってみました。

iPadでの表示: f:id:vincentisambart:20181206092236p:plain

端末 向き 画面全幅③ 読みやすい
最大幅④
余白
(③-④)
左右マージン
((③-④)/2)
iPhone Xs Max
iPhone Xr
iPhone 8 Plus
414 pt 374 pt 40 pt 20 pt
iPhone Xs Max
iPhone Xr
896 pt 672 pt 224 pt 112 pt
iPhone 8 Plus 736 pt 696 pt 40 pt 20 pt
iPhone Xs
iPhone 8
375 pt 343 pt 32 pt 16 pt
iPhone 8 667 pt 627 pt 40 pt 20 pt
iPhone Xs 812 pt 772 pt 40 pt 20 pt
iPhone SE 320 pt 288 pt 32 pt 16 pt
iPhone SE 568 pt 528 pt 40 pt 20 pt
iPad 768 pt 672 pt 96 pt 48 pt
iPad 1024 pt 672 pt 352 pt 176 pt
iPad Pro 11" 834 pt 672 pt 162 pt 81 pt
iPad Pro 11" 1194 pt 672 pt 522 pt 261 pt

上記のサイズは画面の全幅のを使う場合の数字ですが、readableContentGuideが必ず自分のビューのlayoutMarginsGuideに収まるように設計されているので、上記の幅が最大値です。親ビューのマージンがもっと大きかったり、親ビューがもっと小さかったりすると、読みやすい幅が小さくなります。

また、注意すべき点として、readableContentGuidelayoutMarginsGuideの水平方向で中央になるように設計されているので、左右のマージンが違っていれば、readableContentGuideがビューの中央になりません。ただ様々な設定によって(マージンが例えばsafe areaを含むように)マージンが変わるので、指定した左右のマージンが違ってもreadableContentGuideがビューの中央になることもあります。

表の数字をまとめてみると、iPadでは、画面サイズや向きが何であろうと、幅が最大672 ptになるように計算されているようです(上記の表に載っていないiPad Pro 10.5"もiPad Pro 12.9"もそうです)。横向きのiPhone XsやiPhone 8 Plusでは、読みやすい幅がiPadより大きくなるのか少し気になりますが。

縦向きのiPhoneではreadableContentGuideを使うと左右の余白が最低でも16 ptになるように見えます(因みにiOSで殆どのビューの標準のマージンが8 ptです)。ただし、色んなケースを試してみると、親ビューが全画面でない場合、左右の余白が12 ptになったりしますし、layoutMarginsdirectionalLayoutMarginsで(またはInterface Builderで)親ビューのマージンを小さくすると、読みやすい幅の左右の余白がもっと小さくなることがあります。絶対なのはマージンに収まるように設計されているところだけのようですね。

上記の数字にはsafe areaが含まれていません。readableContentGuideをそのまま使うとsafe area外になる可能性があります。本当に画面全体でreadableContentGuideを使う場合、そのreadableContentGuideを持っているビューのinsetsLayoutMarginsFromSafeAreatrueにすれば良いかもしれません。insetsLayoutMarginsFromSafeAreatrueにすると、マージンにsafe areaが含まれるようになり、readableContentGuideがマージン内になるように設計されているので、readableContentGuideもsafe area内になります。因みに、safe area関連の機能は全部iOS 11以上でしか使えません。

Interface Builder

コードでは、readableContentGuideを使って制約を定義すれば良いのですが、ビューをInterface Builderで配置する場合、どうすれば良いのでしょうか。

注意:「Follow Readable Width」の利用を推奨しません。アプリがバックグラウンドにある間にユーザーがDynamic Typeの設定を変えて、アプリに戻るとマージンがちゃんと更新されない場合が多いです。アプリをもう1回バックグラウンドにしてから戻ると更新されますが。

やるべきことが2つあります:

  • 読みやすい幅に合わせたいビューの親ビューのレイアウトの設定に「Follow Readable Width」にチェックを入れます。

    f:id:vincentisambart:20181206090752p:plain:w295

  • 読みやすい幅に合わせたいビューの配置に使われるsuperviewに対する左右の制約がマージンに対する制約である必要があります。
    • 制約の詳細にある「First/Second Item」が「Superview.Leading」や「Superview.Trailing」ではなく、「Superview.Leading Margin」や「Superview.Trailing Margin」である必要があります。変えるには、その「First/Second Item」をクリックして、出てくるメニューに「Relative to margin」を選びましょう。

      f:id:vincentisambart:20181206091213p:plain:w295

    • 「First/Second Item」が「Safe Area」の場合、それをまず「Superview」に変えてから、「Relative to margin」を選びましょう。

      f:id:vincentisambart:20181206091038p:plain:w295

ビューが必ずsafe area内に配置されるようにしたかったら、親ビューのレイアウト設定に「Safe Area Relative Margins」にチェックを入れてください。コード上ではinsetsLayoutMarginsFromSafeAreaと同じです。

Dynamic Type

読みやすい幅は便利そうではありますが、使う前に注意すべき点が1つあります:Dynamic Typeです。

Dynamic Typeというのはユーザーが使われるフォントのサイズを変えられる機能です。iOS全体の設定に一般→アクセシビリティ→さらに大きな文字(英語だとSettings→General→Accessibility→Larger Text)で変えられます。対応しているアプリではフォントサイズがその設定に合わせられます。もっと詳しくはDynamic Typeのまとめ記事をご覧ください。

readableContentGuideのドキュメントを見ると、従われるルールが3つあります:

  • The readable content guide never extends beyond the view’s layout margin guide.
  • The readable content guide is vertically centered inside the layout margin guide.
  • The readable content guide’s width is equal to or less than the readable width defined for the current dynamic text size.

1つめと2つめのルールは既に説明しましたが、3つめはまだでした。上記の表の数字はアクセシビリティ設定で標準のフォントサイズが選択されている場合の値です。もっと大きいフォントサイズが選ばれている場合、読みやすい幅が(マージン内で)広くなります。もっと小さいフォントサイズが選ばれている場合、読みやすい幅が狭くなります。

別の言い方をすると、iOSの「読みやすい幅」の制限が行のポイント数ではなく、その行に入れる文字数と言えるかもしれません。

Dynamic Type対応を既にしてあるアプリは幅が選ばれたフォントサイズに合わせられた方が綺麗に表示されますが、Dynamic Type対応をしていないアプリは大きいフォントサイズが選択されている場合表示が少し読みづらくなりそうです。

ユーザーの選んだフォントサイズカテゴリはUIApplication.shared.preferredContentSizeCategoryで取れます。カテゴリが多いので、どう変わるのかのスクリーンショットは標準サイズ(large)、一番小さいサイズ(extraSmall)、「さらに大きな文字」が有効になっていない場合の一番大きいサイズ(extraExtraExtraLarge)、「さらに大きな文字」が有効になっている場合の一番大きいサイズ(accessibilityExtraExtraExtraLarge)だけのにしました。

  • large (標準設定) f:id:vincentisambart:20181206092236p:plain
  • extraSmall f:id:vincentisambart:20181206092208p:plain
  • extraExtraExtraLarge f:id:vincentisambart:20181206092011p:plain
  • accessibilityExtraExtraExtraLarge f:id:vincentisambart:20181206091716p:plain

読みやすい幅のまとめ表

Dynamic Typeの設定によって読みやすい幅の数字をまとめてみました。上記のスクリーンショット同様4つのカテゴリだけに絞りました。

どう変わるのか見せるために明確な数字を載せていますが、その明確な数字をビューの配置のために使わないでおきましょう。今後iOS端末の画面サイズの種類がまた増えても不思議ではありませんし、今後iOSのメジャーアップデートで数字が少し変わる可能性もあります。ビューをpixel perfectで配置できていた時代はもう終わっています。

iPhone SE

preferred content size category 向き 画面全幅③ 読みやすい
最大幅④
余白
(③-④)
左右マージン
((③-④)/2)
large 320 pt 288 pt 32 pt 16 pt
large 568 pt 528 pt 40 pt 20 pt
extraSmall 320 pt 288 pt 32 pt 16 pt
extraSmall 568 pt 528 pt 40 pt 20 pt
extraExtraExtraLarge 320 pt 288 pt 32 pt 16 pt
extraExtraExtraLarge 568 pt 528 pt 40 pt 20 pt
accessibilityExtraExtraExtraLarge 320 pt 288 pt 32 pt 16 pt
accessibilityExtraExtraExtraLarge 568 pt 528 pt 40 pt 20 pt

フォントサイズが変わっても、読みやすい幅は変わらないようですね。

iPhone 8

preferred content size category 向き 画面全幅③ 読みやすい
最大幅④
余白
(③-④)
左右マージン
((③-④)/2)
large 375 pt 343 pt 32 pt 16 pt
large 667 pt 627 pt 40 pt 20 pt
extraSmall 375 pt 343 pt 32 pt 16 pt
extraSmall 667 pt 560 pt 107 pt 53 pt
extraExtraExtraLarge 375 pt 343 pt 32 pt 16 pt
extraExtraExtraLarge 667 pt 627 pt 40 pt 20 pt
accessibilityExtraExtraExtraLarge 375 pt 343 pt 32 pt 16 pt
accessibilityExtraExtraExtraLarge 667 pt 627 pt 40 pt 20 pt

縦向きの場合、フォントサイズが変わっても、読みやすい幅は変わらないようですが、横向きだと(少し余裕あるので)少し変わるようです。

iPhone 8 Plus

preferred content size category 向き 画面全幅③ 読みやすい
最大幅④
余白
(③-④)
左右マージン
((③-④)/2)
large 414 pt 374 pt 40 pt 20 pt
large 736 pt 696 pt 40 pt 20 pt
extraSmall 414 pt 374 pt 40 pt 20 pt
extraSmall 736 pt 560 pt 176 pt 88 pt
extraExtraExtraLarge 414 pt 374 pt 40 pt 20 pt
extraExtraExtraLarge 736 pt 696 pt 40 pt 20 pt
accessibilityExtraExtraExtraLarge 414 pt 374 pt 40 pt 20 pt
accessibilityExtraExtraExtraLarge 736 pt 696 pt 40 pt 20 pt

iPhone 8同様、縦向きの場合、フォントサイズが変わっても、読みやすい幅は変わらないようですが、横向きだと(少し余裕あるので)少し変わるようです。

iPhone Xs

preferred content size category 向き 画面全幅③ 読みやすい
最大幅④
余白
(③-④)
左右マージン
((③-④)/2)
large 375 pt 343 pt 32 pt 16 pt
large 812 pt 772 pt 40 pt 20 pt
extraSmall 375 pt 343 pt 32 pt 16 pt
extraSmall 812 pt 560 pt 252 pt 126 pt
extraExtraExtraLarge 375 pt 343 pt 32 pt 16 pt
extraExtraExtraLarge 812 pt 772 pt 40 pt 20 pt
accessibilityExtraExtraExtraLarge 375 pt 343 pt 32 pt 16 pt
accessibilityExtraExtraExtraLarge 812 pt 772 pt 40 pt 20 pt

iPhone 8同様、縦向きの場合、フォントサイズが変わっても、読みやすい幅は変わらないようですが、横向きだと(少し余裕あるので)少し変わるようです。

iPhone Xr、iPhone Xs Max

preferred content size category 向き 画面全幅③ 読みやすい
最大幅④
余白
(③-④)
左右マージン
((③-④)/2)
large 414 pt 374 pt 40 pt 20 pt
large 896 pt 672 pt 224 pt 112 pt
extraSmall 414 pt 374 pt 40 pt 20 pt
extraSmall 896 pt 560 pt 336 pt 168 pt
extraExtraExtraLarge 414 pt 374 pt 40 pt 20 pt
extraExtraExtraLarge 896 pt 856 pt 40 pt 20 pt
accessibilityExtraExtraExtraLarge 414 pt 374 pt 40 pt 20 pt
accessibilityExtraExtraExtraLarge 896 pt 856 pt 40 pt 20 pt

他のiPhone同様、縦向きの場合、フォントサイズが変わっても、読みやすい幅は変わらないようですが、横向きの場合、フォントサイズによって(もう少し余裕あるので)それなりに変わるようです。

iPad

preferred content size category 向き 画面全幅③ 読みやすい
最大幅④
余白
(③-④)
左右マージン
((③-④)/2)
large 768 pt 672 pt 96 pt 48 pt
large 1024 pt 672 pt 352 pt 176 pt
extraSmall 768 pt 560 pt 208 pt 104 pt
extraSmall 1024 pt 560 pt 464 pt 232 pt
extraExtraExtraLarge 768 pt 728 pt 40 pt 20 pt
extraExtraExtraLarge 1024 pt 896 pt 128 pt 64 pt
accessibilityExtraExtraExtraLarge 768 pt 728 pt 40 pt 20 pt
accessibilityExtraExtraExtraLarge 1024 pt 984 pt 40 pt 20 pt

小さめなフォントサイズの場合、縦向きと横向きでは幅が同じですが、大きめなフォントサイズの場合そういうわけでもありませんね。

iPad Pro 11"

preferred content size category 向き 画面全幅③ 読みやすい
最大幅④
余白
(③-④)
左右マージン
((③-④)/2)
large 834 pt 672 pt 162 pt 81 pt
large 1194 pt 672 pt 522 pt 261 pt
extraSmall 834 pt 560 pt 274 pt 137 pt
extraSmall 1194 pt 560 pt 634 pt 317 pt
extraExtraExtraLarge 834 pt 794 pt 40 pt 20 pt
extraExtraExtraLarge 1194 pt 896 pt 298 pt 149 pt
accessibilityExtraExtraExtraLarge 834 pt 794 pt 40 pt 20 pt
accessibilityExtraExtraExtraLarge 1194 pt 1154 pt 40 pt 20 pt

通常のiPad同様、小さめなフォントサイズの場合、縦と横では幅が同じですが、大きめなフォントサイズの場合そういうわけでもありませんね。

セル

一番上に載せたiOSの設定画面の例もTwitterの例もテーブルでした。Twitterの方は左右の余白でもセルがタッチに反応するので、読みやすい幅に合わせられたのはテーブル自体ではなく、セル内のコンテンツでしょう。

セルの場合、普通のビューと少し違うところあるので、それを見ましょう。

UITableView

UITableViewのセルの挙動は他のビューと同じです: - コードでビューを配置する場合、readableContentGuideを使えます。 - Interface Builderを使う場合、制約はマージンに対して定義し、親ビュー(おそらくcontent view)のレイアウト設定の「Follow Readable Width」にチェックを入れたら読みやすい幅に合わせられます。

でも、よりよい方法として、UITableViewcellLayoutMarginsFollowReadableWidthというプロパティを使う方法もあります。Interface Builderでは、UITableViewの「Follow Readable Width」にチェックを入れるのと同じです。

cellLayoutMarginsFollowReadableWidthは自分が定義したカスタムセルだけではなく、テーブルの区切り線にもUIKitが提供しているカスタムでないCellStyleにも影響しています。

いくつかのセルCellStyleの一例、cellLayoutMarginsFollowReadableWidthfalseの場合: f:id:vincentisambart:20181206092608p:plain

cellLayoutMarginsFollowReadableWidthtrueの場合: f:id:vincentisambart:20181206092711p:plain

因みにcellLayoutMarginsFollowReadableWidthtrueの場合、content viewのlayoutMarginsGuidereadableContentGuideのどっちを使っても同じことになります。

UICollectionView

UICollectionViewに全幅に渡るセルがあれば、読みやすい幅を使いたくなることあるかもしれません。ただ、UICollectionViewにはcellLayoutMarginsFollowReadableWidthのようなプロパティがありません。

コードではreadableContentGuideは使えますが、Interface Builderでセル(実質セルのcontent view)のレイアウト設定の「Follow Readable Width」にチェックを入れても効果がありません(バグ)。でもワークアラウンドとしてcontent viewの中にビューを入れ、そのビューの「Follow Readable Width」にチェックを入れ、他のビューをその中に入れ、制約をそのビューのマージンに対して定義すれば動くようです。

まとめ

iOSでは、対応すべき画面サイズが少しずつ増えています。先日発表された新しいiPad Proも以前のと画面サイズが少し変わりました。事前に分かっている数少ない画面サイズを元にアプリの配置を決められる時代が終わっています。

その中でできるだけ多くの画面サイズに合わせて綺麗に配置できるツールの1つとして、読みやすい幅、または読みやすいコンテンツ、というのがあります。

読みやすいコンテンツを使うときはドキュメントにも書いてある3つのルールを覚えておきましょう:

  • readableContentGuideが自分のビューのlayoutMarginsGuide外に出ることはありません。
  • readableContentGuideが水平方向でlayoutMarginsGuideの中央にあります。
  • readableContentGuideの幅が選ばれたDynamic Typeのフォントサイズ次第で決まる読みやすい幅以下です。